Updated
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,158 @@
|
||||
import math
|
||||
import os
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Dict, List
|
||||
|
||||
import yaml
|
||||
|
||||
from pdk_registry import PdkRegistry
|
||||
|
||||
|
||||
@dataclass
|
||||
class BuildResult:
|
||||
output_path: str
|
||||
engine: str
|
||||
cells_built: List[str] = field(default_factory=list)
|
||||
warnings: List[str] = field(default_factory=list)
|
||||
|
||||
|
||||
def build_project_gds(project_dir: str, output_path: str, pdk_public_root: str) -> BuildResult:
|
||||
"""Build a hierarchical project GDS from saved cell YAML files."""
|
||||
cells = _load_project_cells(project_dir)
|
||||
if not cells:
|
||||
raise ValueError("No saved cell YAML files found for this project")
|
||||
|
||||
registry = PdkRegistry(pdk_public_root)
|
||||
os.makedirs(os.path.dirname(output_path), exist_ok=True)
|
||||
|
||||
try:
|
||||
return _build_with_gdstk(cells, output_path, registry)
|
||||
except ImportError as gdstk_error:
|
||||
try:
|
||||
return _build_with_nazca(cells, output_path, registry)
|
||||
except ImportError as nazca_error:
|
||||
raise RuntimeError(
|
||||
"Build GDS requires either gdstk or a working nazca installation. "
|
||||
f"gdstk import failed: {gdstk_error}. nazca import failed: {nazca_error}"
|
||||
) from nazca_error
|
||||
|
||||
|
||||
def _load_project_cells(project_dir: str) -> Dict[str, dict]:
|
||||
cells = {}
|
||||
for filename in sorted(os.listdir(project_dir)):
|
||||
if not filename.lower().endswith((".yml", ".yaml")):
|
||||
continue
|
||||
path = os.path.join(project_dir, filename)
|
||||
with open(path, "r", encoding="utf-8") as file:
|
||||
data = yaml.safe_load(file) or {}
|
||||
cell_name = str(data.get("name") or os.path.splitext(filename)[0])
|
||||
cells[cell_name] = data
|
||||
return cells
|
||||
|
||||
|
||||
def _ordered_cell_names(cells: Dict[str, dict]) -> List[str]:
|
||||
composites = [name for name, data in cells.items() if data.get("type") != "project"]
|
||||
projects = [name for name, data in cells.items() if data.get("type") == "project"]
|
||||
return composites + projects
|
||||
|
||||
|
||||
def _build_with_gdstk(cells: Dict[str, dict], output_path: str, registry: PdkRegistry) -> BuildResult:
|
||||
import gdstk
|
||||
|
||||
library = gdstk.Library()
|
||||
built_cells = {}
|
||||
warnings = []
|
||||
|
||||
for cell_name in _ordered_cell_names(cells):
|
||||
data = cells[cell_name]
|
||||
gds_cell = library.new_cell(_safe_cell_name(cell_name, built_cells))
|
||||
built_cells[cell_name] = gds_cell
|
||||
for instance_name, instance in (data.get("instances") or {}).items():
|
||||
component = str(instance.get("component") or "")
|
||||
x = _number(instance.get("x"))
|
||||
y = _number(instance.get("y"))
|
||||
rotation = math.radians(_number(instance.get("rotation")))
|
||||
child = built_cells.get(component)
|
||||
if child is None:
|
||||
asset = registry.resolve(component)
|
||||
if not asset.gds_path:
|
||||
warnings.append(f"Missing GDS for {instance_name}: {component}")
|
||||
continue
|
||||
child = _import_public_gds(gdstk, library, asset.gds_path)
|
||||
gds_cell.add(gdstk.Reference(child, origin=(x, y), rotation=rotation))
|
||||
|
||||
library.write_gds(output_path)
|
||||
return BuildResult(
|
||||
output_path=output_path,
|
||||
engine="gdstk",
|
||||
cells_built=list(built_cells.keys()),
|
||||
warnings=warnings,
|
||||
)
|
||||
|
||||
|
||||
def _import_public_gds(gdstk, library, gds_path: str):
|
||||
source = gdstk.read_gds(gds_path)
|
||||
top_cells = source.top_level()
|
||||
if not top_cells:
|
||||
raise ValueError(f"No top-level cell found in {gds_path}")
|
||||
for source_cell in source.cells:
|
||||
if _library_cell_by_name(library, source_cell.name) is None:
|
||||
library.add(source_cell)
|
||||
return top_cells[0]
|
||||
|
||||
|
||||
def _build_with_nazca(cells: Dict[str, dict], output_path: str, registry: PdkRegistry) -> BuildResult:
|
||||
import nazca as nd
|
||||
|
||||
warnings = []
|
||||
built_cells = {}
|
||||
ordered_names = _ordered_cell_names(cells)
|
||||
for cell_name in ordered_names:
|
||||
data = cells[cell_name]
|
||||
with nd.Cell(cell_name) as current_cell:
|
||||
for instance_name, instance in (data.get("instances") or {}).items():
|
||||
component = str(instance.get("component") or "")
|
||||
x = _number(instance.get("x"))
|
||||
y = _number(instance.get("y"))
|
||||
rotation = _number(instance.get("rotation"))
|
||||
if component in built_cells:
|
||||
built_cells[component].put(x, y, rotation)
|
||||
continue
|
||||
asset = registry.resolve(component)
|
||||
if not asset.gds_path:
|
||||
warnings.append(f"Missing GDS for {instance_name}: {component}")
|
||||
continue
|
||||
loaded = nd.load_gds(asset.gds_path)
|
||||
loaded.put(x, y, rotation)
|
||||
built_cells[cell_name] = current_cell
|
||||
|
||||
top_name = ordered_names[-1]
|
||||
nd.export_gds(built_cells[top_name], filename=output_path)
|
||||
return BuildResult(output_path=output_path, engine="nazca", cells_built=ordered_names, warnings=warnings)
|
||||
|
||||
|
||||
def _safe_cell_name(name: str, existing: dict) -> str:
|
||||
base = "".join(ch if ch.isalnum() or ch in "._$" else "_" for ch in str(name)) or "cell"
|
||||
candidate = base
|
||||
counter = 1
|
||||
used = {cell.name for cell in existing.values()}
|
||||
while candidate in used:
|
||||
counter += 1
|
||||
candidate = f"{base}_{counter}"
|
||||
return candidate
|
||||
|
||||
|
||||
def _library_cell_by_name(library, name: str):
|
||||
for cell in library.cells:
|
||||
if cell.name == name:
|
||||
return cell
|
||||
return None
|
||||
|
||||
|
||||
def _number(value, default=0.0) -> float:
|
||||
try:
|
||||
if value is None or value == "":
|
||||
return default
|
||||
return float(value)
|
||||
except (TypeError, ValueError):
|
||||
return default
|
||||
@@ -0,0 +1,131 @@
|
||||
import os
|
||||
from typing import Dict, Optional
|
||||
|
||||
import yaml
|
||||
|
||||
|
||||
def create_layout_svg_from_gds(yaml_content: str, output_path: str, pdk_registry, project_dir: str = None) -> str:
|
||||
"""Create an SVG preview by placing real public _BB.gds cells from layout YAML."""
|
||||
layout = yaml.safe_load(yaml_content) or {}
|
||||
try:
|
||||
return _create_with_gdstk(layout, output_path, pdk_registry, project_dir)
|
||||
except ImportError as gdstk_error:
|
||||
try:
|
||||
return _create_with_nazca(layout, output_path, pdk_registry, project_dir)
|
||||
except ImportError as nazca_error:
|
||||
raise RuntimeError(
|
||||
"Layout SVG requires GDS geometry support. Install gdstk, or fix nazca dependencies. "
|
||||
f"gdstk import failed: {gdstk_error}. nazca import failed: {nazca_error}"
|
||||
) from nazca_error
|
||||
|
||||
|
||||
def _create_with_gdstk(layout: dict, output_path: str, pdk_registry, project_dir: Optional[str]) -> str:
|
||||
import gdstk
|
||||
|
||||
library = gdstk.Library()
|
||||
cell_cache = {}
|
||||
top = _build_gdstk_cell(gdstk, library, layout, pdk_registry, project_dir, cell_cache)
|
||||
os.makedirs(os.path.dirname(output_path), exist_ok=True)
|
||||
top.write_svg(output_path)
|
||||
return output_path
|
||||
|
||||
|
||||
def _build_gdstk_cell(gdstk, library, layout: dict, pdk_registry, project_dir: Optional[str], cell_cache: Dict):
|
||||
cell_name = _safe_cell_name(layout.get("name") or "layout", library)
|
||||
top = library.new_cell(cell_name)
|
||||
|
||||
for instance_name, instance in (layout.get("instances") or {}).items():
|
||||
component = str(instance.get("component") or "")
|
||||
x = _number(instance.get("x"))
|
||||
y = _number(instance.get("y"))
|
||||
rotation = _number(instance.get("rotation")) * 3.141592653589793 / 180
|
||||
child = _resolve_child_cell(gdstk, library, component, pdk_registry, project_dir, cell_cache)
|
||||
if child is None:
|
||||
raise FileNotFoundError(f"Unable to resolve _BB.gds for instance {instance_name}: {component}")
|
||||
top.add(gdstk.Reference(child, origin=(x, y), rotation=rotation))
|
||||
|
||||
return top
|
||||
|
||||
|
||||
def _resolve_child_cell(gdstk, library, component: str, pdk_registry, project_dir: Optional[str], cell_cache: Dict):
|
||||
if component in cell_cache:
|
||||
return cell_cache[component]
|
||||
|
||||
local_layout = _load_local_layout(component, project_dir)
|
||||
if local_layout is not None:
|
||||
child = _build_gdstk_cell(gdstk, library, local_layout, pdk_registry, project_dir, cell_cache)
|
||||
cell_cache[component] = child
|
||||
return child
|
||||
|
||||
asset = pdk_registry.resolve(component)
|
||||
if not asset.gds_path:
|
||||
return None
|
||||
child = _import_gds_cell(gdstk, library, asset.gds_path)
|
||||
cell_cache[component] = child
|
||||
return child
|
||||
|
||||
|
||||
def _import_gds_cell(gdstk, library, gds_path: str):
|
||||
source = gdstk.read_gds(gds_path)
|
||||
top_cells = source.top_level()
|
||||
if not top_cells:
|
||||
raise ValueError(f"No top-level cell found in {gds_path}")
|
||||
for source_cell in source.cells:
|
||||
if _library_cell_by_name(library, source_cell.name) is None:
|
||||
library.add(source_cell)
|
||||
return top_cells[0]
|
||||
|
||||
|
||||
def _create_with_nazca(layout: dict, output_path: str, pdk_registry, project_dir: Optional[str]) -> str:
|
||||
import nazca as nd
|
||||
|
||||
png_path = os.path.splitext(output_path)[0] + ".gds"
|
||||
with nd.Cell(str(layout.get("name") or "layout")) as top:
|
||||
for instance_name, instance in (layout.get("instances") or {}).items():
|
||||
component = str(instance.get("component") or "")
|
||||
asset = pdk_registry.resolve(component)
|
||||
if not asset.gds_path:
|
||||
raise FileNotFoundError(f"Unable to resolve _BB.gds for instance {instance_name}: {component}")
|
||||
loaded = nd.load_gds(asset.gds_path)
|
||||
loaded.put(_number(instance.get("x")), _number(instance.get("y")), _number(instance.get("rotation")))
|
||||
nd.export_gds(top, filename=png_path)
|
||||
raise RuntimeError(
|
||||
"Nazca can build the placed GDS, but SVG preview export requires gdstk in this backend."
|
||||
)
|
||||
|
||||
|
||||
def _load_local_layout(component: str, project_dir: Optional[str]) -> Optional[dict]:
|
||||
if not project_dir or "/" in component or "\\" in component or component == "generate_with_forge":
|
||||
return None
|
||||
for ext in (".yml", ".yaml"):
|
||||
path = os.path.join(project_dir, f"{component}{ext}")
|
||||
if os.path.exists(path):
|
||||
with open(path, "r", encoding="utf-8") as file:
|
||||
return yaml.safe_load(file) or {}
|
||||
return None
|
||||
|
||||
|
||||
def _safe_cell_name(name, library) -> str:
|
||||
base = "".join(ch if ch.isalnum() or ch in "._$" else "_" for ch in str(name)) or "layout"
|
||||
candidate = base
|
||||
counter = 1
|
||||
while _library_cell_by_name(library, candidate) is not None:
|
||||
counter += 1
|
||||
candidate = f"{base}_{counter}"
|
||||
return candidate
|
||||
|
||||
|
||||
def _library_cell_by_name(library, name: str):
|
||||
for cell in library.cells:
|
||||
if cell.name == name:
|
||||
return cell
|
||||
return None
|
||||
|
||||
|
||||
def _number(value, default=0.0) -> float:
|
||||
try:
|
||||
if value is None or value == "":
|
||||
return default
|
||||
return float(value)
|
||||
except (TypeError, ValueError):
|
||||
return default
|
||||
@@ -0,0 +1,87 @@
|
||||
import os
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
|
||||
import yaml
|
||||
|
||||
|
||||
@dataclass
|
||||
class PdkAsset:
|
||||
component: str
|
||||
yaml_path: Optional[str] = None
|
||||
gds_path: Optional[str] = None
|
||||
metadata: Optional[dict] = None
|
||||
|
||||
|
||||
class PdkRegistry:
|
||||
"""Resolve public PDK component names to metadata and public GDS assets."""
|
||||
|
||||
def __init__(self, public_root: str):
|
||||
self.public_root = os.path.abspath(public_root)
|
||||
self._asset_cache = {}
|
||||
|
||||
def resolve(self, component: str) -> PdkAsset:
|
||||
key = (component or "").strip().replace("\\", "/").strip("/")
|
||||
if not key:
|
||||
return PdkAsset(component=component)
|
||||
if key in self._asset_cache:
|
||||
return self._asset_cache[key]
|
||||
|
||||
yaml_path = self._find_yaml(key)
|
||||
gds_path = self._find_gds(key, yaml_path)
|
||||
metadata = self._load_yaml(yaml_path) if yaml_path else None
|
||||
asset = PdkAsset(component=component, yaml_path=yaml_path, gds_path=gds_path, metadata=metadata)
|
||||
self._asset_cache[key] = asset
|
||||
return asset
|
||||
|
||||
def _find_yaml(self, key: str) -> Optional[str]:
|
||||
direct = os.path.join(self.public_root, *key.split("/"))
|
||||
candidates = []
|
||||
if direct.lower().endswith((".yml", ".yaml")):
|
||||
candidates.append(direct)
|
||||
else:
|
||||
name = key.split("/")[-1]
|
||||
candidates.append(os.path.join(direct, f"{name}.yml"))
|
||||
candidates.append(os.path.join(direct, f"{name}.yaml"))
|
||||
|
||||
for candidate in candidates:
|
||||
if self._inside_root(candidate) and os.path.exists(candidate):
|
||||
return os.path.abspath(candidate)
|
||||
|
||||
name = key.split("/")[-1]
|
||||
for root, dirs, files in os.walk(self.public_root):
|
||||
if os.path.basename(root) == name:
|
||||
for filename in files:
|
||||
if filename.lower().endswith((".yml", ".yaml")):
|
||||
return os.path.join(root, filename)
|
||||
dirs.clear()
|
||||
return None
|
||||
|
||||
def _find_gds(self, key: str, yaml_path: Optional[str]) -> Optional[str]:
|
||||
search_dir = os.path.dirname(yaml_path) if yaml_path else os.path.join(self.public_root, *key.split("/"))
|
||||
name = key.split("/")[-1]
|
||||
candidates = [
|
||||
os.path.join(search_dir, f"{name}_BB.gds"),
|
||||
os.path.join(search_dir, f"{name}.gds"),
|
||||
]
|
||||
for candidate in candidates:
|
||||
if self._inside_root(candidate) and os.path.exists(candidate):
|
||||
return os.path.abspath(candidate)
|
||||
if os.path.isdir(search_dir):
|
||||
for filename in sorted(os.listdir(search_dir)):
|
||||
if filename.lower().endswith("_bb.gds"):
|
||||
return os.path.join(search_dir, filename)
|
||||
for filename in sorted(os.listdir(search_dir)):
|
||||
if filename.lower().endswith(".gds"):
|
||||
return os.path.join(search_dir, filename)
|
||||
return None
|
||||
|
||||
def _load_yaml(self, yaml_path: Optional[str]) -> Optional[dict]:
|
||||
if not yaml_path:
|
||||
return None
|
||||
with open(yaml_path, "r", encoding="utf-8") as file:
|
||||
return yaml.safe_load(file) or {}
|
||||
|
||||
def _inside_root(self, path: str) -> bool:
|
||||
target = os.path.abspath(path)
|
||||
return target == self.public_root or target.startswith(self.public_root + os.sep)
|
||||
+112
-4
@@ -10,18 +10,29 @@ from flask import Flask, jsonify, send_from_directory, request, redirect, url_fo
|
||||
from werkzeug.security import check_password_hash
|
||||
import database
|
||||
from flask import Response
|
||||
from gds_builder import build_project_gds
|
||||
from layout_preview import create_layout_svg_from_gds
|
||||
from pdk_registry import PdkRegistry
|
||||
from technology_manifest import TechnologyManifestError, read_technology_manifest
|
||||
|
||||
# --- Path Configurations ---
|
||||
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
FRONTEND_DIR = os.path.join(BASE_DIR, '..', 'frontend')
|
||||
|
||||
REPO_ROOT = os.path.abspath(os.path.join(BASE_DIR, '..', '..'))
|
||||
PDK_PUBLIC_ROOT = os.path.abspath(os.environ.get(
|
||||
'MXPIC_PDK_PUBLIC_ROOT',
|
||||
os.path.join(REPO_ROOT, 'opt_pdk_public', 'foundries')
|
||||
))
|
||||
EDA_PDK_ROOT = os.path.abspath(os.path.join(BASE_DIR, '..', 'mxpic', 'PDKs'))
|
||||
YML_PATH = os.path.join(BASE_DIR, '..', 'mxpic', 'PDKs', 'Silterra', 'directories.yaml')
|
||||
COMPS_ROOT = os.path.join(BASE_DIR, '..', 'mxpic', 'PDKs', 'Silterra')
|
||||
COMPS_ROOT = os.environ.get('MXPIC_COMPONENT_ROOT', PDK_PUBLIC_ROOT)
|
||||
# Define where your new icons folder is located (adjust if it's placed elsewhere)
|
||||
ICONS_DIR = os.path.join(BASE_DIR, 'icons')
|
||||
|
||||
#build layout save path
|
||||
DATABASE_ROOT = os.path.abspath(os.path.join(BASE_DIR, '..', 'database'))
|
||||
PDK_REGISTRY = PdkRegistry(PDK_PUBLIC_ROOT)
|
||||
|
||||
|
||||
app = Flask(__name__, template_folder=FRONTEND_DIR, static_folder=FRONTEND_DIR)
|
||||
@@ -101,6 +112,14 @@ def cell_file_path(project_name, cell_name):
|
||||
return os.path.join(project_root(project_name), f"{safe_name(cell_name, 'canvas_1')}.yml")
|
||||
|
||||
|
||||
def cell_svg_path(project_name, cell_name):
|
||||
return os.path.join(project_root(project_name), f"{safe_name(cell_name, 'canvas_1')}.svg")
|
||||
|
||||
|
||||
def project_gds_path(project_name):
|
||||
return os.path.join(project_root(project_name), f"{safe_name(project_name, 'project_1')}.gds")
|
||||
|
||||
|
||||
def project_meta_path(project_name):
|
||||
return os.path.join(project_root(project_name), ".project.json")
|
||||
|
||||
@@ -118,6 +137,14 @@ def write_project_meta(project_name, meta):
|
||||
with open(project_meta_path(project_name), 'w', encoding='utf-8') as f:
|
||||
json.dump(meta, f, indent=2)
|
||||
|
||||
|
||||
def ensure_project_path(project_name):
|
||||
layout_root = os.path.abspath(user_layout_root())
|
||||
target = os.path.abspath(project_root(project_name))
|
||||
if target != layout_root and not target.startswith(layout_root + os.sep):
|
||||
raise ValueError("Invalid project path")
|
||||
return target
|
||||
|
||||
# ... [Keep countSpaces and buildTree exactly as they are] ...
|
||||
|
||||
def findComps(baseDir):
|
||||
@@ -271,7 +298,7 @@ def health_check():
|
||||
def list_technologies():
|
||||
"""List technology choices from mxpic/PDKs/<foundry>/<technology>."""
|
||||
technologies = []
|
||||
pdks_root = os.path.join(BASE_DIR, '..', 'mxpic', 'PDKs')
|
||||
pdks_root = EDA_PDK_ROOT
|
||||
if os.path.isdir(pdks_root):
|
||||
for foundry in sorted(os.listdir(pdks_root)):
|
||||
foundry_path = os.path.join(pdks_root, foundry)
|
||||
@@ -290,6 +317,20 @@ def list_technologies():
|
||||
return jsonify({"technologies": technologies})
|
||||
|
||||
|
||||
@app.route('/api/technologies/<foundry>/<technology>/manifest', methods=['GET'])
|
||||
@login_required_json
|
||||
def get_technology_manifest(foundry, technology):
|
||||
try:
|
||||
manifest = read_technology_manifest(
|
||||
EDA_PDK_ROOT,
|
||||
safe_name(foundry, ''),
|
||||
safe_name(technology, '')
|
||||
)
|
||||
return jsonify({"manifest": manifest})
|
||||
except TechnologyManifestError as e:
|
||||
return jsonify({"error": str(e)}), 404
|
||||
|
||||
|
||||
@app.route('/api/profile', methods=['GET', 'PATCH'])
|
||||
@login_required_json
|
||||
def account_profile():
|
||||
@@ -526,16 +567,83 @@ def save_layout():
|
||||
with open(save_path, 'w', encoding='utf-8') as f:
|
||||
f.write(content)
|
||||
|
||||
record_action('layout.save', project=project, cell=cell, detail={"bytes": len(content)})
|
||||
svg_path = cell_svg_path(project, cell)
|
||||
create_layout_svg_from_gds(content, svg_path, pdk_registry=PDK_REGISTRY, project_dir=project_root(project))
|
||||
|
||||
record_action('layout.save', project=project, cell=cell, detail={"bytes": len(content), "svg": svg_path})
|
||||
return jsonify({
|
||||
"message": "successfully saved",
|
||||
"project": project,
|
||||
"cell": cell,
|
||||
"path": save_path
|
||||
"path": save_path,
|
||||
"svg_path": svg_path,
|
||||
"svg_url": url_for('get_layout_svg', project_name=project, cell_name=cell)
|
||||
}), 200
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
|
||||
@app.route('/api/projects/<project_name>/cells/<cell_name>/layout.svg')
|
||||
@login_required_json
|
||||
def get_layout_svg(project_name, cell_name):
|
||||
try:
|
||||
project_dir = ensure_project_path(project_name)
|
||||
svg_path = os.path.abspath(cell_svg_path(project_name, cell_name))
|
||||
if not svg_path.startswith(project_dir + os.sep):
|
||||
return jsonify({"error": "Invalid SVG path"}), 400
|
||||
if not os.path.exists(svg_path):
|
||||
return jsonify({"error": "Layout SVG not found"}), 404
|
||||
return no_cache_response(send_from_directory(os.path.dirname(svg_path), os.path.basename(svg_path), mimetype='image/svg+xml'))
|
||||
except ValueError as e:
|
||||
return jsonify({"error": str(e)}), 400
|
||||
|
||||
|
||||
@app.route('/api/build-gds', methods=['POST'])
|
||||
@login_required_json
|
||||
def build_gds():
|
||||
data = request.get_json(silent=True) or {}
|
||||
project = safe_name(data.get('project'), 'project_1')
|
||||
try:
|
||||
project_dir = ensure_project_path(project)
|
||||
if not os.path.isdir(project_dir):
|
||||
return jsonify({"error": "Project not found"}), 404
|
||||
output_path = project_gds_path(project)
|
||||
result = build_project_gds(project_dir, output_path, PDK_PUBLIC_ROOT)
|
||||
record_action('gds.build', project=project, detail={
|
||||
"path": result.output_path,
|
||||
"engine": result.engine,
|
||||
"warnings": result.warnings
|
||||
})
|
||||
return jsonify({
|
||||
"message": "GDS built",
|
||||
"project": project,
|
||||
"path": result.output_path,
|
||||
"gds_url": url_for('get_project_gds', project_name=project, filename=os.path.basename(result.output_path)),
|
||||
"engine": result.engine,
|
||||
"cells_built": result.cells_built,
|
||||
"warnings": result.warnings
|
||||
})
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
|
||||
@app.route('/api/projects/<project_name>/gds/<filename>')
|
||||
@login_required_json
|
||||
def get_project_gds(project_name, filename):
|
||||
try:
|
||||
project_dir = ensure_project_path(project_name)
|
||||
safe_filename = safe_name(filename, f"{safe_name(project_name, 'project_1')}.gds")
|
||||
if not safe_filename.lower().endswith('.gds'):
|
||||
return jsonify({"error": "Invalid GDS filename"}), 400
|
||||
gds_path = os.path.abspath(os.path.join(project_dir, safe_filename))
|
||||
if not gds_path.startswith(project_dir + os.sep):
|
||||
return jsonify({"error": "Invalid GDS path"}), 400
|
||||
if not os.path.exists(gds_path):
|
||||
return jsonify({"error": "GDS not found"}), 404
|
||||
return send_from_directory(project_dir, safe_filename, as_attachment=True)
|
||||
except ValueError as e:
|
||||
return jsonify({"error": str(e)}), 400
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import os
|
||||
|
||||
import yaml
|
||||
|
||||
|
||||
class TechnologyManifestError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def technology_manifest_path(pdks_root: str, foundry: str, technology: str) -> str:
|
||||
base = os.path.abspath(pdks_root)
|
||||
path = os.path.abspath(os.path.join(base, foundry, technology, "technology.yml"))
|
||||
if not path.startswith(base + os.sep):
|
||||
raise TechnologyManifestError("Invalid technology path")
|
||||
return path
|
||||
|
||||
|
||||
def read_technology_manifest(pdks_root: str, foundry: str, technology: str) -> dict:
|
||||
path = technology_manifest_path(pdks_root, foundry, technology)
|
||||
if not os.path.exists(path):
|
||||
raise TechnologyManifestError("technology manifest not generated; run mxpic_forge technology export workflow")
|
||||
with open(path, "r", encoding="utf-8") as file:
|
||||
manifest = yaml.safe_load(file) or {}
|
||||
if not isinstance(manifest.get("xsections"), dict):
|
||||
raise TechnologyManifestError("technology manifest is missing xsections")
|
||||
if not isinstance(manifest.get("defaults"), dict):
|
||||
raise TechnologyManifestError("technology manifest is missing defaults")
|
||||
return manifest
|
||||
Reference in New Issue
Block a user