Programm architecture simplified so that the EDA can now run individually without generation of .gds file
This commit is contained in:
+52
-43
@@ -1,6 +1,6 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
# Description: Flask backend API server for authentication, project management, PDK library access, layout preview, and GDS build endpoints.
|
||||
# Inside functions: no_cache_response, login_required_json, wrapper, request_ip, record_action, safe_name, user_layout_root, project_root, cell_file_path, cell_svg_path, cell_routes_path, write_route_points_sidecar, project_gds_path, technology_manifest_path_for_project, current_pdk_root, current_pdk_registry, scoped_pdk_root_for_project, pdk_root_for_request_project, project_meta_path, read_project_meta
|
||||
# Inside functions: no_cache_response, login_required_json, wrapper, request_ip, record_action, safe_name, user_layout_root, project_root, cell_file_path, cell_svg_path, cell_routes_path, write_route_points_sidecar, project_gds_path, technology_manifest_path_for_project, current_pdk_root, scoped_pdk_root_for_project, pdk_root_for_request_project, project_meta_path, read_project_meta
|
||||
# Developer : Qin Yue @ 2026
|
||||
# Organization : OptiHK Limited
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -17,15 +17,14 @@ 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 pdk_access import (
|
||||
cleanup_expired_exports,
|
||||
create_export_path,
|
||||
pdk_root_for_session,
|
||||
prefer_full_gds_for_session,
|
||||
)
|
||||
from routed_layout_preview import create_routed_layout_svg, layout_has_links
|
||||
from router_dependency import RouterStackUnavailable
|
||||
from routed_layout_preview import create_routed_layout_svg
|
||||
from technology_manifest import TechnologyManifestError, read_technology_manifest
|
||||
|
||||
# --- Path Configurations ---
|
||||
@@ -35,12 +34,6 @@ 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')
|
||||
# Component/category icons are served from backend/icons for the library panel.
|
||||
ICONS_DIR = os.path.join(BASE_DIR, 'icons')
|
||||
|
||||
@@ -181,27 +174,32 @@ def project_gds_path(project_name):
|
||||
return os.path.join(project_root(project_name), f"{safe_name(project_name, 'project_1')}.gds")
|
||||
|
||||
|
||||
def technology_manifest_path_for_project(project_name):
|
||||
"""Return the stored technology manifest path for a project."""
|
||||
technology_id = read_project_meta(project_name).get("technology") or ""
|
||||
if "/" not in technology_id:
|
||||
return None
|
||||
foundry, technology = technology_id.split("/", 1)
|
||||
path = os.path.abspath(os.path.join(EDA_PDK_ROOT, safe_name(foundry, ''), safe_name(technology, ''), "technology.yml"))
|
||||
# Keep stored project metadata from escaping the local EDA PDK root.
|
||||
if path.startswith(EDA_PDK_ROOT + os.sep) and os.path.exists(path):
|
||||
return path
|
||||
return None
|
||||
|
||||
|
||||
def current_pdk_root():
|
||||
"""Resolve the active PDK root for the current request session."""
|
||||
return pdk_root_for_session(session, REPO_ROOT)
|
||||
|
||||
|
||||
def current_pdk_registry():
|
||||
"""Create a PDK registry configured for the current session scope."""
|
||||
return PdkRegistry(current_pdk_root(), prefer_full_gds=prefer_full_gds_for_session(session))
|
||||
def technology_manifest_path_for_pdk_root(pdk_root, foundry, technology):
|
||||
"""Return a safe technology.yml path under a role-scoped PDK root."""
|
||||
base_root = os.path.abspath(pdk_root)
|
||||
path = os.path.abspath(os.path.join(
|
||||
base_root,
|
||||
safe_name(foundry, ''),
|
||||
safe_name(technology, ''),
|
||||
"technology.yml"
|
||||
))
|
||||
if path.startswith(base_root + os.sep) and os.path.exists(path):
|
||||
return path
|
||||
return None
|
||||
|
||||
|
||||
def technology_manifest_path_for_project(project_name):
|
||||
"""Return the stored technology manifest path for a project and session."""
|
||||
technology_id = read_project_meta(project_name).get("technology") or ""
|
||||
if "/" not in technology_id:
|
||||
return None
|
||||
foundry, technology = technology_id.split("/", 1)
|
||||
return technology_manifest_path_for_pdk_root(current_pdk_root(), foundry, technology)
|
||||
|
||||
|
||||
def scoped_pdk_root_for_project(project_name):
|
||||
@@ -265,6 +263,14 @@ def ensure_project_path(project_name):
|
||||
# These helpers turn the active PDK folder structure into the nested component
|
||||
# library tree shown in the canvas sidebar.
|
||||
|
||||
def component_yml_files(files):
|
||||
"""Return component metadata YAML files, excluding technology manifests."""
|
||||
return [
|
||||
f for f in files
|
||||
if f.lower().endswith(('.yml', '.yaml')) and f.lower() != "technology.yml"
|
||||
]
|
||||
|
||||
|
||||
def findComps(baseDir, path_root=None):
|
||||
"""Scan component folders, return map of paths -> component info."""
|
||||
compMap = {}
|
||||
@@ -273,7 +279,7 @@ def findComps(baseDir, path_root=None):
|
||||
# A folder containing a YAML file is treated as a component leaf; scanning
|
||||
# stops below that leaf so nested assets do not pollute the library tree.
|
||||
for root, dirs, files in os.walk(baseDir):
|
||||
ymlFiles = [f for f in files if f.endswith('.yml')]
|
||||
ymlFiles = component_yml_files(files)
|
||||
if ymlFiles:
|
||||
parentDir = os.path.dirname(root)
|
||||
relPath = os.path.relpath(parentDir, refDir)
|
||||
@@ -356,7 +362,7 @@ def readCompYaml(compName, comps_root=None):
|
||||
for root, dirs, files in os.walk(search_root):
|
||||
if os.path.basename(root) == compName:
|
||||
dirs.clear()
|
||||
ymlFiles = [f for f in files if f.endswith('.yml')]
|
||||
ymlFiles = component_yml_files(files)
|
||||
if ymlFiles:
|
||||
ymlPath = os.path.join(root, ymlFiles[0])
|
||||
with open(ymlPath, 'r', encoding='utf-8') as f:
|
||||
@@ -445,11 +451,11 @@ def health_check():
|
||||
@app.route('/api/technologies', methods=['GET'])
|
||||
@login_required_json
|
||||
def list_technologies():
|
||||
"""List technology choices from mxpic/PDKs/<foundry>/<technology>."""
|
||||
"""List technology choices from the active role-scoped PDK root."""
|
||||
technologies = []
|
||||
pdks_root = EDA_PDK_ROOT
|
||||
# Technology choices are built from directory names because each technology
|
||||
# folder owns its generated technology.yml manifest.
|
||||
pdks_root = current_pdk_root()
|
||||
# Technology choices are built from foundry/technology directories under the
|
||||
# role PDK root, and only folders with technology.yml are exposed.
|
||||
if os.path.isdir(pdks_root):
|
||||
for foundry in sorted(os.listdir(pdks_root)):
|
||||
foundry_path = os.path.join(pdks_root, foundry)
|
||||
@@ -459,6 +465,8 @@ def list_technologies():
|
||||
technology_path = os.path.join(foundry_path, technology)
|
||||
if not os.path.isdir(technology_path):
|
||||
continue
|
||||
if not os.path.exists(os.path.join(technology_path, "technology.yml")):
|
||||
continue
|
||||
technologies.append({
|
||||
"foundry": foundry,
|
||||
"technology": technology,
|
||||
@@ -473,11 +481,7 @@ def list_technologies():
|
||||
def get_technology_manifest(foundry, technology):
|
||||
"""Return the routing and layer manifest for a selected technology."""
|
||||
try:
|
||||
manifest = read_technology_manifest(
|
||||
EDA_PDK_ROOT,
|
||||
safe_name(foundry, ''),
|
||||
safe_name(technology, '')
|
||||
)
|
||||
manifest = read_technology_manifest(current_pdk_root(), safe_name(foundry, ''), safe_name(technology, ''))
|
||||
return jsonify({"manifest": manifest})
|
||||
except TechnologyManifestError as e:
|
||||
return jsonify({"error": str(e)}), 404
|
||||
@@ -735,11 +739,11 @@ def save_layout():
|
||||
write_route_points_sidecar(content, cell_routes_path(project, cell))
|
||||
|
||||
svg_path = None
|
||||
preview_status = "not_requested"
|
||||
preview_error = None
|
||||
if create_preview:
|
||||
svg_path = cell_svg_path(project, cell)
|
||||
# Routed layouts need the router backend; placement-only layouts can
|
||||
# use the simpler GDS/SVG preview path.
|
||||
if layout_has_links(content):
|
||||
try:
|
||||
create_routed_layout_svg(
|
||||
content,
|
||||
svg_path,
|
||||
@@ -748,8 +752,11 @@ def save_layout():
|
||||
technology_manifest_path=technology_manifest_path_for_project(project),
|
||||
prefer_full_gds=prefer_full_gds_for_session(session),
|
||||
)
|
||||
else:
|
||||
create_layout_svg_from_gds(content, svg_path, pdk_registry=current_pdk_registry(), project_dir=project_root(project))
|
||||
preview_status = "generated"
|
||||
except RouterStackUnavailable as e:
|
||||
preview_status = "skipped"
|
||||
preview_error = str(e)
|
||||
svg_path = None
|
||||
|
||||
record_action('layout.save', project=project, cell=cell, detail={"bytes": len(content), "svg": svg_path})
|
||||
return jsonify({
|
||||
@@ -758,7 +765,9 @@ def save_layout():
|
||||
"cell": cell,
|
||||
"path": save_path,
|
||||
"svg_path": svg_path,
|
||||
"svg_url": url_for('get_layout_svg', project_name=project, cell_name=cell) if svg_path else None
|
||||
"svg_url": url_for('get_layout_svg', project_name=project, cell_name=cell) if svg_path else None,
|
||||
"preview_status": preview_status,
|
||||
"preview_error": preview_error
|
||||
}), 200
|
||||
|
||||
except Exception as e:
|
||||
|
||||
Reference in New Issue
Block a user