|
|
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 |
|
|
|