127 lines
4.2 KiB
Python
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"])
|