Files
mxpic_EDA/backend/layout_files.py
2026-06-10 19:10:59 +08:00

127 lines
4.2 KiB
Python

# -----------------------------------------------------------------------------
# Description: Layout YAML file filtering, parsing, and staging helpers.
# Inside functions: is_layout_cell_filename, parse_layout_cell_content, read_layout_cell_file, load_layout_cell_files, layout_cell_filename, write_layout_cells_to_directory
# Developer : Qin Yue @ 2026
# Organization : OptiHK Limited
# -----------------------------------------------------------------------------
import os
import re
import yaml
LAYOUT_YAML_EXTENSIONS = (".yml", ".yaml")
ROUTE_SIDECAR_SUFFIXES = (".routes.yml", ".routes.yaml")
class LayoutFileError(ValueError):
"""Raised when a saved layout YAML file cannot be used as a cell."""
def is_layout_cell_filename(filename):
"""Return True for user layout cell YAML files, excluding sidecars/manifests."""
lower = (filename or "").lower()
if lower == "technology.yml":
return False
if lower.endswith(ROUTE_SIDECAR_SUFFIXES):
return False
return lower.endswith(LAYOUT_YAML_EXTENSIONS)
def is_layout_cell_document(data):
"""Return True when parsed YAML looks like an mxPIC saved cell/project."""
if not isinstance(data, dict):
return False
kind = data.get("kind")
if kind and str(kind).strip().lower() != "cell":
return False
layout_keys = {
"schema_version",
"canvas_size",
"canvasSize",
"instances",
"elements",
"pins",
"ports",
"bundles",
}
return any(key in data for key in layout_keys)
def parse_layout_cell_content(content, source="layout YAML"):
"""Parse and validate saved layout YAML content."""
try:
data = yaml.safe_load(content or "") or {}
except yaml.YAMLError as exc:
raise LayoutFileError(f"{source} is not valid YAML: {exc}") from exc
if not is_layout_cell_document(data):
raise LayoutFileError(f"{source} is not a saved mxPIC layout cell")
return data
def read_layout_cell_file(path):
"""Read a saved layout cell file and return parsed data plus raw content."""
try:
with open(path, "r", encoding="utf-8") as file:
content = file.read()
except OSError as exc:
raise LayoutFileError(f"{os.path.basename(path)} could not be read: {exc}") from exc
return parse_layout_cell_content(content, os.path.basename(path)), content
def load_layout_cell_files(project_dir):
"""Load valid layout cell files from a project directory and collect warnings."""
cells = []
warnings = []
if not os.path.isdir(project_dir):
return cells, warnings
for filename in sorted(os.listdir(project_dir)):
if not is_layout_cell_filename(filename):
continue
path = os.path.join(project_dir, filename)
if not os.path.isfile(path):
continue
try:
data, content = read_layout_cell_file(path)
except LayoutFileError as exc:
warnings.append(f"Skipped {filename}: {exc}")
continue
cells.append({
"filename": filename,
"name": str(data.get("name") or os.path.splitext(filename)[0]),
"data": data,
"content": content,
})
return cells, warnings
def layout_cell_filename(cell_name, fallback="canvas_1.yml"):
"""Build a safe filename for staging a parsed layout cell."""
name = re.sub(r"[^A-Za-z0-9_.-]+", "_", str(cell_name or "")).strip("._")
if not name:
return fallback
return f"{name}.yml"
def write_layout_cells_to_directory(cells, output_dir):
"""Write valid layout cell contents into a clean staging directory."""
os.makedirs(output_dir, exist_ok=True)
used_names = set()
for index, cell in enumerate(cells, start=1):
filename = layout_cell_filename(cell.get("name"), cell.get("filename") or f"cell_{index}.yml")
base, ext = os.path.splitext(filename)
unique_filename = filename
counter = 1
while unique_filename.lower() in used_names:
counter += 1
unique_filename = f"{base}_{counter}{ext}"
used_names.add(unique_filename.lower())
with open(os.path.join(output_dir, unique_filename), "w", encoding="utf-8") as file:
file.write(cell["content"])