# Copyright 2025 Mohamed Aly
# SPDX-License-Identifier: Apache-2.0
"""Unified configuration and component registry for ALY projects.
This module provides the main ProjectConfig class that loads and manages
all configuration from the .aly/ directory and discovers project components
through manifest.yaml files.
Configuration structure:
- config.yaml: Project info, features, defaults, paths
- toolchains.yaml: Firmware toolchain definitions
- sim.yaml: Simulation tool settings
- synth.yaml: Synthesis settings
- lint.yaml: Lint settings
- constraints.yaml: Design constraints
- fpga.yaml: FPGA programming settings
Components (RTL, testbenches, firmware, IP) are discovered via manifest.yaml
files in their respective directories.
"""
import yaml
from pathlib import Path
from typing import Dict, Any, Optional, List, Type, NoReturn
from dataclasses import dataclass, field
from aly.config.models import (
ProjectInfo,
FeatureFlags,
DefaultsConfig,
PathsConfig,
SimConfig,
SynthConfig,
LintConfig,
ConstraintsConfig,
FPGAConfig,
SimToolConfig,
Toolchain,
RTLManifest,
RTLModule,
TestbenchManifest,
Testbench,
TestSuite,
FirmwareManifest,
FirmwareBuild,
IPManifest,
)
from aly.config.models.helpers import RTLPackage
from aly.log import dbg, wrn, err, die
# Manifest filename constant
MANIFEST_FILENAME = "manifest.yaml"
@dataclass
class UnitRef:
kind: str # "rtl_module", "testbench", "firmware_build", ...
name: str # module/testbench/build name
manifest_type: str # "rtl", "testbench", "firmware", ...
manifest_name: str # parent manifest name
manifest: Any # parent manifest object
obj: Any # the module/testbench/build object
[docs]
class ProjectConfig:
"""
Unified configuration manager and component registry for ALY projects.
Loads configuration from .aly/ directory and discovers components through
manifest.yaml files in configured paths. Provides unified access to all
project settings and components.
Example:
config = ProjectConfig.load(".")
# Access project info
print(config.info.name)
# Get RTL block
cpu = config.get_rtl_block("cpu")
# Get testbench
tb = config.get_testbench("tb_alu")
# List all components
rtl_blocks = config.list_rtl_blocks()
# Resolve dependencies
deps = config.resolve_rtl_deps(tb)
"""
CONFIG_DIR = ".aly"
# Config file list
CONFIG_FILES = {
"config": "config.yaml",
"toolchains": "toolchains.yaml",
"sim": "sim.yaml",
"synth": "synth.yaml",
"lint": "lint.yaml",
"constraints": "constraints.yaml",
"fpga": "fpga.yaml",
}
[docs]
def __init__(self, project_root: Path):
"""Initialize configuration manager."""
self.project_root = project_root
self.config_dir = project_root / self.CONFIG_DIR
# Core configuration (always loaded)
self.info: Optional[ProjectInfo] = None
self.features: Optional[FeatureFlags] = None
self.defaults: Optional[DefaultsConfig] = None
self.paths: Optional[PathsConfig] = None
# Tool configurations (lazy loaded)
self._sim: Optional[SimConfig] = None
self._synth: Optional[SynthConfig] = None
self._lint: Optional[LintConfig] = None
self._constraints: Optional[ConstraintsConfig] = None
self._fpga: Optional[FPGAConfig] = None
# Component registry (lazy loaded)
self._components: Dict[str, Dict[str, Any]] = {}
self._toolchains: Dict[str, Toolchain] = {}
self._discovered: bool = False
# Raw data cache
self._raw: Dict[str, Any] = {}
# New: registry of units
self._units: Dict[str, Dict[str, UnitRef]] = {
"rtl_module": {},
"testbench": {},
"firmware_build": {},
"package": {},
}
# Initialize component type buckets
for component_type in ["rtl", "ip", "testbench", "firmware"]:
self._components[component_type] = {}
# =========================================================================
# Loading and Initialization
# =========================================================================
[docs]
@classmethod
def load(cls, project_root: Path | str) -> "ProjectConfig":
"""
Load configuration from project root.
Args:
project_root: Path to project root directory
Returns:
Loaded ProjectConfig instance
Raises:
SystemExit: If configuration is missing or invalid
"""
if isinstance(project_root, str):
project_root = Path(project_root)
config = cls(project_root)
if not config.config_dir.exists():
die(
f"Configuration directory not found: {config.config_dir}\n"
f"Run 'aly init' to initialize a project."
)
main_config = config.config_dir / "config.yaml"
if not main_config.exists():
die(f"Main configuration file not found: {main_config}")
config._load_main_config()
config._load_tool_configs()
config._load_toolchains()
return config
def _load_yaml(self, path: Path) -> Dict[str, Any]:
"""Load YAML file safely."""
if not path.exists():
return {}
try:
with open(path, encoding="utf-8") as f:
data = yaml.safe_load(f)
return data if data else {}
except yaml.YAMLError as e:
die(f"Invalid YAML in {path}: {e}")
def _load_main_config(self):
"""Load main config.yaml file."""
config_path = self.config_dir / "config.yaml"
data = self._load_yaml(config_path)
self._raw["config"] = data
self.info = ProjectInfo.from_dict(data.get("project", {}))
self.features = FeatureFlags.from_dict(data.get("features", {}))
self.defaults = DefaultsConfig.from_dict(data.get("defaults", {}))
self.paths = PathsConfig.from_dict(data.get("paths", {}))
def _load_tool_configs(self):
"""Load tool configuration files."""
tool_map = {
"sim": ("_sim", SimConfig),
"synth": ("_synth", SynthConfig),
"lint": ("_lint", LintConfig),
"constraints": ("_constraints", ConstraintsConfig),
"fpga": ("_fpga", FPGAConfig),
}
for name, (attr, config_cls) in tool_map.items():
filename = self.CONFIG_FILES.get(name)
if filename:
file_path = self.config_dir / filename
if file_path.exists():
data = self._load_yaml(file_path)
self._raw[name] = data
setattr(self, attr, config_cls.from_dict(data, self.project_root))
def _load_toolchains(self) -> None:
"""Load toolchain configurations from .aly/toolchains.yaml."""
toolchains_path = self.config_dir / "toolchains.yaml"
if not toolchains_path.exists():
dbg("No toolchains.yaml found")
return
try:
data = self._load_yaml(toolchains_path)
toolchains_data = data.get("toolchains", {})
for name, tc_data in toolchains_data.items():
if isinstance(tc_data, dict):
try:
self._toolchains[name] = Toolchain.from_dict(name, tc_data)
except Exception as e:
wrn(f"Failed to parse toolchain {name}: {e}")
except Exception as e:
wrn(f"Failed to load toolchains.yaml: {e}")
# =========================================================================
# Component Discovery
# =========================================================================
[docs]
def discover_all(self) -> None:
"""Scan all configured paths for manifest.yaml files.
This is the main discovery method that populates the component registry.
It's called lazily on first access to any get/list method.
"""
if self._discovered:
return
dbg(f"Starting component discovery in {self.project_root}")
# Map path types to expected manifest types
path_type_mapping = {
"rtl": ["rtl"],
"tb": ["testbench"],
"ip": ["ip"],
"firmware": ["firmware"],
}
for path_type, expected_types in path_type_mapping.items():
paths = self.paths.as_dict().get(path_type, []) if self.paths else []
if isinstance(paths, str):
paths = [paths]
for path in paths:
self._scan_directory(path, expected_types)
self._discovered = True
dbg(
f"Discovery complete: {sum(len(v) for v in self._components.values())} components"
)
def _scan_directory(
self, path: str, expected_types: Optional[List[str]] = None
) -> None:
"""Recursively scan a directory for manifest.yaml files.
Args:
path: Relative path from project root to scan
expected_types: Optional list of expected manifest types
"""
root = self.project_root / path
if not root.exists():
dbg(f"Path does not exist: {root}")
return
# Find all manifest.yaml files recursively
for manifest_path in root.rglob(MANIFEST_FILENAME):
try:
manifest = self._load_manifest(manifest_path)
if manifest:
self._register(manifest)
# For IPs, also scan nested directories
manifest_type = getattr(manifest, "type", None)
if manifest_type == "ip":
self._scan_ip_subdirs(manifest_path.parent)
except Exception as e:
wrn(f"Failed to load manifest {manifest_path}: {e}")
def _scan_ip_subdirs(self, ip_root: Path) -> None:
"""Scan IP subdirectories for nested manifests.
Args:
ip_root: Path to the IP directory (parent of manifest.yaml)
"""
subdirs = ["rtl", "tb", "fw", "firmware"]
for subdir in subdirs:
subdir_path = ip_root / subdir
if subdir_path.exists():
for manifest_path in subdir_path.rglob(MANIFEST_FILENAME):
try:
manifest = self._load_manifest(manifest_path)
if manifest:
self._register(manifest)
except Exception as e:
wrn(f"Failed to load IP sub-manifest {manifest_path}: {e}")
def _load_manifest(self, path: Path) -> Optional[Any]:
"""Load and parse a manifest.yaml file.
Args:
path: Path to the manifest.yaml file
Returns:
Typed manifest object or None if type is unknown
"""
try:
with open(path, encoding="utf-8") as f:
data = yaml.safe_load(f) or {}
except Exception as e:
wrn(f"Failed to parse YAML in {path}: {e}")
return None
manifest_type = data.get("type")
if not manifest_type:
dbg(f"Manifest has no type field: {path}")
return None
loaders: Dict[str, Type] = {
"rtl": RTLManifest,
"ip": IPManifest,
"testbench": TestbenchManifest,
"firmware": FirmwareManifest,
}
loader_class = loaders.get(manifest_type)
if loader_class is None:
dbg(f"Unknown manifest type '{manifest_type}' in {path}")
return None
try:
if manifest_type == "rtl":
return loader_class.load(path)
else:
return loader_class.from_dict(data, path)
except Exception as e:
wrn(f"Failed to create {manifest_type} manifest from {path}: {e}")
return None
def _register(self, manifest: Any) -> None:
"""Register a manifest in the appropriate type bucket.
Args:
manifest: Manifest object with `type` and `name` attributes
"""
manifest_type = getattr(manifest, "type", None)
manifest_name = getattr(manifest, "name", None)
if not manifest_type or not manifest_name:
wrn(f"Cannot register manifest without type or name: {manifest}")
return
if manifest_name in self._components.get(manifest_type, {}):
dbg(
f"Duplicate {manifest_type} manifest: {manifest_name} (keeping first)"
)
return
self._components[manifest_type][manifest_name] = manifest
dbg(f"Registered {manifest_type}: {manifest_name}")
# --- Register units inside the manifest -------------------------------
# 1) RTL modules
if manifest_type == "rtl" and hasattr(manifest, "modules"):
for module in manifest.modules:
key = module.name # or f"{manifest_name}:{module.name}" if you want namespaced
if key in self._units["rtl_module"]:
wrn(
f"Duplicate RTL module name '{key}' "
f"(from manifest {manifest_name})"
)
continue
self._units["rtl_module"][key] = UnitRef(
kind="rtl_module",
name=module.name,
manifest_type="rtl",
manifest_name=manifest_name,
manifest=manifest,
obj=module,
)
# 2) Testbenches (once TestbenchManifest has a .testbenches list)
if manifest_type == "testbench" and hasattr(manifest, "testbenches"):
for tb in manifest.testbenches:
key = tb.name
if key in self._units["testbench"]:
wrn(
f"Duplicate testbench name '{key}' "
f"(from manifest {manifest_name})"
)
continue
self._units["testbench"][key] = UnitRef(
kind="testbench",
name=tb.name,
manifest_type="testbench",
manifest_name=manifest_name,
manifest=manifest,
obj=tb,
)
# 3) Firmware builds (once FirmwareManifest has a .builds or similar)
if manifest_type == "firmware" and hasattr(manifest, "builds"):
for build in manifest.builds:
key = build.name # or build.id / build.target, depending on your schema
if key in self._units["firmware_build"]:
wrn(
f"Duplicate firmware build name '{key}' "
f"(from manifest {manifest_name})"
)
continue
self._units["firmware_build"][key] = UnitRef(
kind="firmware_build",
name=key,
manifest_type="firmware",
manifest_name=manifest_name,
manifest=manifest,
obj=build,
)
# 4) Named packages (from RTL manifests)
if manifest_type == "rtl" and hasattr(manifest, "packages"):
for pkg in manifest.packages:
if not pkg.name:
continue # skip unnamed packages
key = pkg.name
if key in self._units["package"]:
wrn(
f"Duplicate package name '{key}' "
f"(from manifest {manifest_name})"
)
continue
self._units["package"][key] = UnitRef(
kind="package",
name=pkg.name,
manifest_type="rtl",
manifest_name=manifest_name,
manifest=manifest,
obj=pkg,
)
# =========================================================================
# Manifest Access (low-level, manifest-centric)
# =========================================================================
def _ensure_discovered(self) -> None:
"""Ensure manifests and units have been discovered."""
if not self._discovered:
self.discover_all()
[docs]
def get(self, manifest_type: str, name: str) -> Optional[Any]:
"""Get a manifest by type and name.
Args:
manifest_type: Component type (rtl, ip, testbench, firmware, test_suite)
name: Manifest name
Returns:
Manifest object or None if not found.
"""
self._ensure_discovered()
return self._components.get(manifest_type, {}).get(name)
[docs]
def list(self, manifest_type: str) -> List[str]:
"""List all manifest names of a specific type.
Args:
manifest_type: Component type
Returns:
List of manifest names.
"""
self._ensure_discovered()
return list(self._components.get(manifest_type, {}).keys())
[docs]
def get_all(self, manifest_type: str) -> Dict[str, Any]:
"""Get all manifests of a specific type.
Args:
manifest_type: Component type
Returns:
Dict mapping name to manifest object.
"""
self._ensure_discovered()
return self._components.get(manifest_type, {}).copy()
# =========================================================================
# Convenience Methods for Components (unit-centric)
# =========================================================================
# ------------------------- Generic unit access ---------------------------
[docs]
def get_unit(self, kind: str, name: str) -> Optional[UnitRef]:
"""Get a unit (rtl_module, testbench, firmware_build, ...) by kind and name."""
self._ensure_discovered()
return self._units.get(kind, {}).get(name)
[docs]
def list_units(self, kind: str) -> List[str]:
"""List all unit names of a given kind."""
self._ensure_discovered()
return list(self._units.get(kind, {}).keys())
# ------------------------- RTL module units -----------------------------
[docs]
def get_rtl_module(self, name: str) -> Optional[UnitRef]:
"""Get a single RTL module unit by name."""
return self.get_unit("rtl_module", name)
[docs]
def list_rtl_modules(self) -> List[str]:
"""List all RTL module unit names."""
return self.list_units("rtl_module")
[docs]
def iter_rtl_modules(self):
"""Iterate over all RTL module units.
Yields:
UnitRef objects for each RTL module.
"""
self._ensure_discovered()
return self._units["rtl_module"].values()
[docs]
def get_rtl_module_objects(self) -> Dict[str, tuple[RTLModule, RTLManifest]]:
"""Get all RTL modules with their parent manifests.
Returns:
Dict: module_name -> (module_obj, parent_manifest)
"""
self._ensure_discovered()
result: Dict[str, tuple[RTLModule, RTLManifest]] = {}
for name, ref in self._units["rtl_module"].items():
result[name] = (ref.obj, ref.manifest) # obj is RTLModule, manifest is RTLManifest
return result
[docs]
def get_rtl_module_files(self, module_name: str) -> List[Path]:
"""Get compilation files (packages + module files) for a given RTL module.
Module is looked up project-wide, by name.
"""
ref = self.get_rtl_module(module_name)
if not ref:
return []
module: RTLModule = ref.obj
manifest: RTLManifest = ref.manifest
return manifest.get_files_for_module(module.name)
[docs]
def get_rtl_module_package_files(self, module_name: str) -> List[Path]:
"""Get package files that apply to a given RTL module."""
ref = self.get_rtl_module(module_name)
if not ref:
return []
module: RTLModule = ref.obj
manifest: RTLManifest = ref.manifest
return manifest.get_pkg_files_for_module(module.name)
# ------------------------- Testbench units ------------------------------
[docs]
def get_testbench_unit(self, name: str) -> Optional[UnitRef]:
"""Get a single testbench unit by name.
Requires that TestbenchManifest exposes a `testbenches` list and that
`_register()` populates `_units["testbench"]`.
"""
return self.get_unit("testbench", name)
[docs]
def list_testbench_units(self) -> List[str]:
"""List all testbench unit names."""
return self.list_units("testbench")
[docs]
def get_testbench(self, name: str) -> Optional[Any]:
"""Get a testbench by name (returns Testbench object).
This is a convenience method that returns the testbench object directly,
rather than the UnitRef wrapper.
"""
ref = self.get_testbench_unit(name)
return ref.obj if ref else None
[docs]
def list_testbenches(self) -> List[str]:
"""List all testbench names.
Convenience alias for list_testbench_units().
"""
return self.list_testbench_units()
[docs]
def iter_testbenches(self):
"""Iterate over all testbench units.
Yields:
UnitRef objects for each testbench.
"""
self._ensure_discovered()
return self._units["testbench"].values()
# ------------------------- Test suite access -----------------------------
[docs]
def get_testsuite(self, name: str) -> Optional[Any]:
"""Get a test suite by name (searches all testbench manifests).
Returns:
TestSuite object or None if not found.
"""
self._ensure_discovered()
for manifest in self._components.get("testbench", {}).values():
if hasattr(manifest, "testsuites"):
for suite in manifest.testsuites:
if suite.name == name:
return suite
return None
[docs]
def list_testsuites(self) -> List[str]:
"""List all test suite names."""
self._ensure_discovered()
names = []
for manifest in self._components.get("testbench", {}).values():
if hasattr(manifest, "testsuites"):
names.extend(s.name for s in manifest.testsuites)
return names
# ------------------------- Dependency Resolution -------------------------
[docs]
def resolve_rtl_deps(self, unit: Any, _visited: Optional[set] = None) -> List[UnitRef]:
"""Recursively resolve RTL dependencies for a unit (testbench or RTL module).
Args:
unit: Testbench or RTLModule object with dependencies list
_visited: Internal set to track visited modules and prevent cycles
Returns:
List of UnitRef objects (containing both RTLModule obj and RTLManifest manifest)
in dependency order (dependencies before dependents)
"""
if _visited is None:
_visited = set()
resolved = []
deps = getattr(unit, "dependencies", [])
for dep in deps:
if isinstance(dep, dict) and dep.get("type") == "rtl":
dep_name = dep.get("name", "")
if not dep_name:
continue
# Skip if already visited (avoid cycles)
if dep_name in _visited:
continue
ref = self.get_rtl_module(dep_name)
if ref:
_visited.add(dep_name)
# Recursively resolve dependencies of this RTL module
module: RTLModule = ref.obj
sub_deps = self.resolve_rtl_deps(module, _visited)
# Add sub-dependencies first (topological order)
for sub_ref in sub_deps:
if sub_ref not in resolved:
resolved.append(sub_ref)
# Then add this dependency
if ref not in resolved:
resolved.append(ref)
return resolved
[docs]
def resolve_package_deps(self, testbench: Any) -> List[UnitRef]:
"""Resolve package dependencies for a testbench.
Args:
testbench: Testbench object with dependencies list
Returns:
List of UnitRef objects (containing RTLPackage obj and parent RTLManifest)
"""
resolved = []
deps = getattr(testbench, "dependencies", [])
for dep in deps:
if isinstance(dep, dict) and dep.get("type") == "package":
ref = self.get_package_unit(dep.get("name", ""))
if ref:
resolved.append(ref)
return resolved
[docs]
def resolve_package_dep_files(self, testbench: Any) -> List[Path]:
"""Resolve package dependencies for a testbench and return file paths.
Args:
testbench: Testbench object with dependencies list
Returns:
List of resolved Paths for package files.
"""
files: List[Path] = []
seen: set[Path] = set()
for ref in self.resolve_package_deps(testbench):
pkg: RTLPackage = ref.obj
manifest: RTLManifest = ref.manifest
p = manifest.resolve_path(pkg.path)
if p.exists() and p not in seen:
files.append(p)
seen.add(p)
return files
# ------------------------- Package units ----------------------------------
[docs]
def get_package_unit(self, name: str) -> Optional[UnitRef]:
"""Get a package unit by name.
Args:
name: The package name to look up.
Returns:
UnitRef containing the RTLPackage obj and parent RTLManifest.
"""
return self.get_unit("package", name)
[docs]
def get_package(self, name: str) -> Optional[RTLPackage]:
"""Get a package by name (returns RTLPackage object).
This is a convenience method that returns the package object directly,
rather than the UnitRef wrapper.
"""
ref = self.get_package_unit(name)
return ref.obj if ref else None
[docs]
def list_packages(self) -> List[str]:
"""List all named package names."""
return self.list_units("package")
[docs]
def iter_packages(self):
"""Iterate over all package units.
Yields:
UnitRef objects for each named package.
"""
self._ensure_discovered()
return self._units["package"].values()
[docs]
def get_package_file(self, name: str) -> Optional[Path]:
"""Get the resolved file path for a package by name.
Args:
name: The package name to look up.
Returns:
Resolved Path if found and exists, None otherwise.
"""
ref = self.get_package_unit(name)
if not ref:
return None
pkg: RTLPackage = ref.obj
manifest: RTLManifest = ref.manifest
p = manifest.resolve_path(pkg.path)
return p if p.exists() else None
[docs]
def get_package_files_by_names(self, names: List[str]) -> List[Path]:
"""Get package files by their names (for dependency resolution).
Args:
names: List of package names to include.
Returns:
List of resolved paths for the named packages.
"""
files: List[Path] = []
seen: set[Path] = set()
for name in names:
p = self.get_package_file(name)
if p and p not in seen:
files.append(p)
seen.add(p)
return files
# ------------------------- Firmware build units -------------------------
[docs]
def get_firmware_build(self, name: str) -> Optional[UnitRef]:
"""Get a single firmware build unit by name.
Requires that FirmwareManifest exposes a `builds` list and that
`_register()` populates `_units["firmware_build"]`.
"""
return self.get_unit("firmware_build", name)
[docs]
def list_firmware_builds(self) -> List[str]:
"""List all firmware build unit names."""
return self.list_units("firmware_build")
[docs]
def get_firmware(self, name: str) -> Optional[FirmwareBuild]:
"""Get a firmware build by name (returns FirmwareBuild object).
This is a convenience method that returns the build object directly,
rather than the UnitRef wrapper.
"""
ref = self.get_firmware_build(name)
return ref.obj if ref else None
[docs]
def list_firmware(self) -> List[str]:
"""List all firmware build names.
Convenience alias for list_firmware_builds().
"""
return self.list_firmware_builds()
[docs]
def iter_firmware_builds(self):
"""Iterate over all firmware build units.
Yields:
UnitRef objects for each firmware build.
"""
self._ensure_discovered()
return self._units["firmware_build"].values()
# ------------------------- Toolchain access ------------------------------
# ------------------------- IP manifests (no units) ----------------------
[docs]
def get_ip(self, name: str) -> Optional[IPManifest]:
"""Get an IP manifest by name."""
return self.get("ip", name)
[docs]
def list_ips(self) -> List[str]:
"""List all discovered IP manifest names."""
return self.list("ip")
# =========================================================================
# Configuration Access (Lazy-Loaded Properties)
# =========================================================================
@property
def sim(self) -> SimConfig:
"""Simulation configuration."""
if self._sim is None:
self._sim = SimConfig(_project_root=self.project_root)
return self._sim
@property
def synth(self) -> SynthConfig:
"""Synthesis configuration."""
if self._synth is None:
self._synth = SynthConfig(_project_root=self.project_root)
return self._synth
@property
def lint(self) -> LintConfig:
"""Lint configuration."""
if self._lint is None:
self._lint = LintConfig(_project_root=self.project_root)
return self._lint
@property
def constraints(self) -> ConstraintsConfig:
"""Constraints configuration."""
if self._constraints is None:
self._constraints = ConstraintsConfig(_project_root=self.project_root)
return self._constraints
@property
def fpga(self) -> FPGAConfig:
"""FPGA configuration."""
if self._fpga is None:
self._fpga = FPGAConfig(_project_root=self.project_root)
return self._fpga
# =========================================================================
# Utility Methods
# =========================================================================
[docs]
def is_enabled(self, feature: str) -> bool:
"""Check if a feature is enabled."""
if self.features:
return self.features.is_enabled(feature)
return False
[docs]
def resolve_path(self, path: str | Path) -> Path:
"""Resolve path relative to project root."""
p = Path(path)
if p.is_absolute():
return p
return (self.project_root / p).resolve()
[docs]
def refresh(self) -> None:
"""Force re-discovery of all manifests."""
self._discovered = False
self._components = {
t: {} for t in ["rtl", "ip", "testbench", "firmware"]
}
self._units = {
"rtl_module": {},
"testbench": {},
"firmware_build": {},
"package": {},
}
self._toolchains = {}
self.discover_all()
[docs]
def summary(self) -> Dict[str, int]:
"""Get a summary of discovered components by type.
Returns:
Dict mapping component type to count
"""
if not self._discovered:
self.discover_all()
return {t: len(v) for t, v in self._components.items()}
[docs]
def validate_all(self) -> Dict[str, List[Any]]:
"""Validate all discovered components.
Returns:
Dict mapping component type to list of validation messages
"""
from aly.config.models import ValidationLevel
results = {}
for component_type in ["rtl", "ip", "testbench", "firmware"]:
components = self.get_all(component_type)
all_messages = []
for name, manifest in components.items():
messages = manifest.validate()
errors = [m for m in messages if m.level == ValidationLevel.ERROR]
if errors:
all_messages.extend(errors)
if all_messages:
results[component_type] = all_messages
return results
def __repr__(self) -> str:
if not self.info:
return f"ProjectConfig(not loaded, root={self.project_root})"
if self._discovered:
counts = ", ".join(
f"{t}={len(v)}" for t, v in self._components.items() if v
)
return f"ProjectConfig({self.info.name} v{self.info.version}, {counts})"
else:
return f"ProjectConfig({self.info.name} v{self.info.version}, not discovered)"