Programm architecture simplified so that the EDA can now run individually without generation of .gds file

This commit is contained in:
2026-06-01 09:19:44 +08:00
parent 78f38d3be7
commit 7cf618fe02
160 changed files with 1640 additions and 11497 deletions
+52 -43
View File
@@ -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: