| | from __future__ import annotations |
| |
|
| | import os |
| | import shutil |
| | import shlex |
| | import subprocess |
| | import time |
| | from dataclasses import dataclass |
| | from typing import Dict, List, Optional, Tuple |
| |
|
| | from edgeeda.utils import ensure_dir |
| |
|
| |
|
| | @dataclass |
| | class RunResult: |
| | return_code: int |
| | runtime_sec: float |
| | cmd: str |
| | stdout: str |
| | stderr: str |
| | |
| | def is_success(self) -> bool: |
| | """Check if the run was successful.""" |
| | return self.return_code == 0 |
| | |
| | def error_summary(self, max_lines: int = 5) -> str: |
| | """Extract key error information from stderr.""" |
| | if self.is_success(): |
| | return "Success" |
| | |
| | lines = self.stderr.split('\n') |
| | |
| | error_lines = [ |
| | l for l in lines |
| | if any(kw in l.lower() for kw in ['error', 'fatal', 'failed', 'exception']) |
| | ] |
| | |
| | if error_lines: |
| | return '\n'.join(error_lines[-max_lines:]) |
| | |
| | |
| | if lines: |
| | return '\n'.join(lines[-max_lines:]) |
| | |
| | return f"Command failed with return code {self.return_code}" |
| |
|
| |
|
| | class ORFSRunner: |
| | """ |
| | Minimal ORFS interface: |
| | - Runs `make <target> DESIGN_CONFIG=... FLOW_VARIANT=... VAR=...` |
| | - Uses ORFS_FLOW_DIR (OpenROAD-flow-scripts/flow) as working directory. |
| | """ |
| |
|
| | def __init__(self, orfs_flow_dir: str): |
| | self.flow_dir = os.path.abspath(orfs_flow_dir) |
| | if not os.path.isdir(self.flow_dir): |
| | raise FileNotFoundError(f"ORFS flow dir not found: {self.flow_dir}") |
| | self._openroad_fallback = os.path.abspath( |
| | os.path.join(self.flow_dir, "..", "tools", "install", "OpenROAD", "bin", "openroad") |
| | ) |
| | self._opensta_fallback = os.path.abspath( |
| | os.path.join(self.flow_dir, "..", "tools", "install", "OpenROAD", "bin", "sta") |
| | ) |
| | self._yosys_fallback = os.path.abspath( |
| | os.path.join(self.flow_dir, "..", "tools", "install", "yosys", "bin", "yosys") |
| | ) |
| |
|
| | def _build_env(self) -> Dict[str, str]: |
| | env = os.environ.copy() |
| | openroad_exe = env.get("OPENROAD_EXE") |
| | if not openroad_exe or not os.path.isfile(openroad_exe) or not os.access(openroad_exe, os.X_OK): |
| | if os.path.isfile(self._openroad_fallback) and os.access(self._openroad_fallback, os.X_OK): |
| | env["OPENROAD_EXE"] = self._openroad_fallback |
| | else: |
| | found = shutil.which("openroad") |
| | if found: |
| | env["OPENROAD_EXE"] = found |
| | opensta_exe = env.get("OPENSTA_EXE") |
| | if not opensta_exe or not os.path.isfile(opensta_exe) or not os.access(opensta_exe, os.X_OK): |
| | if os.path.isfile(self._opensta_fallback) and os.access(self._opensta_fallback, os.X_OK): |
| | env["OPENSTA_EXE"] = self._opensta_fallback |
| | else: |
| | found = shutil.which("sta") |
| | if found: |
| | env["OPENSTA_EXE"] = found |
| | yosys_exe = env.get("YOSYS_EXE") |
| | if not yosys_exe or not os.path.isfile(yosys_exe) or not os.access(yosys_exe, os.X_OK): |
| | if os.path.isfile(self._yosys_fallback) and os.access(self._yosys_fallback, os.X_OK): |
| | env["YOSYS_EXE"] = self._yosys_fallback |
| | else: |
| | found = shutil.which("yosys") |
| | if found: |
| | env["YOSYS_EXE"] = found |
| | return env |
| |
|
| | def run_make( |
| | self, |
| | target: str, |
| | design_config: str, |
| | flow_variant: str, |
| | overrides: Dict[str, str], |
| | timeout_sec: Optional[int] = None, |
| | extra_make_args: Optional[List[str]] = None, |
| | max_retries: int = 0, |
| | ) -> RunResult: |
| | """ |
| | Run make command with optional retry logic. |
| | |
| | Args: |
| | target: Make target (e.g., 'synth', 'place', 'route') |
| | design_config: Design configuration path |
| | flow_variant: Flow variant identifier |
| | overrides: Dictionary of make variable overrides |
| | timeout_sec: Timeout in seconds |
| | extra_make_args: Additional make arguments |
| | max_retries: Maximum number of retries for transient failures |
| | |
| | Returns: |
| | RunResult with command execution details |
| | """ |
| | extra_make_args = extra_make_args or [] |
| | |
| | cmd_list = [ |
| | "make", |
| | target, |
| | f"DESIGN_CONFIG={design_config}", |
| | f"FLOW_VARIANT={flow_variant}", |
| | ] |
| | for k, v in overrides.items(): |
| | cmd_list.append(f"{k}={v}") |
| | cmd_list += extra_make_args |
| |
|
| | cmd_str = " ".join(shlex.quote(x) for x in cmd_list) |
| |
|
| | |
| | last_result = None |
| | for attempt in range(max_retries + 1): |
| | t0 = time.time() |
| | try: |
| | env = self._build_env() |
| | p = subprocess.run( |
| | cmd_list, |
| | cwd=self.flow_dir, |
| | capture_output=True, |
| | text=True, |
| | timeout=timeout_sec, |
| | env=env, |
| | ) |
| | dt = time.time() - t0 |
| | result = RunResult( |
| | return_code=p.returncode, |
| | runtime_sec=dt, |
| | cmd=cmd_str, |
| | stdout=p.stdout[-20000:], |
| | stderr=p.stderr[-20000:], |
| | ) |
| | |
| | |
| | if result.is_success() or attempt >= max_retries: |
| | return result |
| | |
| | last_result = result |
| | |
| | |
| | if attempt < max_retries: |
| | wait_time = 2 ** attempt |
| | time.sleep(wait_time) |
| | |
| | except subprocess.TimeoutExpired: |
| | dt = time.time() - t0 |
| | result = RunResult( |
| | return_code=124, |
| | runtime_sec=dt, |
| | cmd=cmd_str, |
| | stdout="", |
| | stderr=f"Command timed out after {timeout_sec} seconds", |
| | ) |
| | if attempt >= max_retries: |
| | return result |
| | last_result = result |
| | if attempt < max_retries: |
| | time.sleep(2 ** attempt) |
| | except Exception as e: |
| | dt = time.time() - t0 |
| | result = RunResult( |
| | return_code=1, |
| | runtime_sec=dt, |
| | cmd=cmd_str, |
| | stdout="", |
| | stderr=f"Exception during execution: {str(e)}", |
| | ) |
| | if attempt >= max_retries: |
| | return result |
| | last_result = result |
| | if attempt < max_retries: |
| | time.sleep(2 ** attempt) |
| | |
| | return last_result |
| |
|