Skip to content

Commit 3d0bc18

Browse files
oharboeclaude
andcommitted
Make KLayout an optional dependency
KLayout is a heavy end-user GUI tool that was previously required to complete the ORFS flow. The `finish` target depended on GDS_FINAL_FILE and `do-finish` called `do-gds`, meaning the flow could not complete without KLayout installed — even though most use cases never need GDS. This change decouples KLayout so it is only pulled in when explicitly requested via `make gds` / `make do-gds`, following the existing target naming pattern (place/do-place, finish/do-finish, gds/do-gds). Makefile changes: - Remove $(GDS_FINAL_FILE) from `finish` dependencies - Remove do-gds from `do-finish` recipe - Add explicit `gds` phony target - Add `check-klayout` guard on all KLayout-dependent targets (GDS merge, DRC, LVS, gallery, klayout viewer shortcuts) Extract .lyt tech file generation from sed to Python: - The do-klayout and do-klayout_wrap targets previously used fragile sed-based XML manipulation with shell variable expansion and ifeq/else branching. New util/generate_klayout_tech.py replaces this with stdlib xml/regex processing (no KLayout dependency). Refactor def2stream.py for testability: - Extract logic into merge_gds() function with pya as a parameter - Guard `import pya` so the module can be imported without KLayout - When run via `klayout -r`, the script behaves identically using klayout's -rd global variables The interface is backwards compatible: `make finish gds` produces GDS as before, `do-gds` is unchanged, and bazel-orfs can invoke `do-gds` from a new `orfs_gds()` rule while `do-final` no longer requires KLayout in the toolchain. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: Øyvind Harboe <oyvind.harboe@zylin.com>
1 parent 6df88fe commit 3d0bc18

4 files changed

Lines changed: 274 additions & 102 deletions

File tree

flow/Makefile

Lines changed: 28 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -195,21 +195,25 @@ $(OBJECTS_DIR)/klayout.lyt: $(KLAYOUT_TECH_FILE) $(OBJECTS_DIR)/klayout_tech.lef
195195

196196
.PHONY: do-klayout
197197
do-klayout:
198-
ifeq ($(KLAYOUT_ENV_VAR_IN_PATH),valid)
199-
SC_LEF_RELATIVE_PATH="$(shell realpath --relative-to=$(RESULTS_DIR) $(SC_LEF))"; \
200-
OTHER_LEFS_RELATIVE_PATHS=$$(echo "$(foreach file, $(OBJECTS_DIR)/klayout_tech.lef $(ADDITIONAL_LEFS),<lef-files>$$(realpath --relative-to=$(RESULTS_DIR) $(file))</lef-files>)"); \
201-
sed 's,<lef-files>.*</lef-files>,<lef-files>'"$$SC_LEF_RELATIVE_PATH"'</lef-files>'"$$OTHER_LEFS_RELATIVE_PATHS"',g' $(KLAYOUT_TECH_FILE) > $(OBJECTS_DIR)/klayout.lyt
202-
else
203-
sed 's,<lef-files>.*</lef-files>,$(foreach file, $(OBJECTS_DIR)/klayout_tech.lef $(SC_LEF) $(ADDITIONAL_LEFS),<lef-files>$(shell realpath --relative-to=$(RESULTS_DIR) $(file))</lef-files>),g' $(KLAYOUT_TECH_FILE) > $(OBJECTS_DIR)/klayout.lyt
204-
endif
205-
sed -i 's,<map-file>.*</map-file>,$(foreach file, $(FLOW_HOME)/platforms/$(PLATFORM)/*map,<map-file>$(shell realpath $(file))</map-file>),g' $(OBJECTS_DIR)/klayout.lyt
198+
@mkdir -p $(dir $(OBJECTS_DIR)/klayout.lyt)
199+
$(PYTHON_EXE) $(UTILS_DIR)/generate_klayout_tech.py \
200+
--template $(KLAYOUT_TECH_FILE) \
201+
--output $(OBJECTS_DIR)/klayout.lyt \
202+
--lef-files $(OBJECTS_DIR)/klayout_tech.lef $(SC_LEF) $(ADDITIONAL_LEFS) \
203+
--reference-dir $(RESULTS_DIR) \
204+
--map-files $(wildcard $(FLOW_HOME)/platforms/$(PLATFORM)/*map)
206205

207206
$(OBJECTS_DIR)/klayout_wrap.lyt: $(KLAYOUT_TECH_FILE) $(OBJECTS_DIR)/klayout_tech.lef
208207
$(UNSET_AND_MAKE) do-klayout_wrap
209208

210209
.PHONY: do-klayout_wrap
211210
do-klayout_wrap:
212-
sed 's,<lef-files>.*</lef-files>,$(foreach file, $(OBJECTS_DIR)/klayout_tech.lef $(WRAP_LEFS),<lef-files>$(shell realpath --relative-to=$(OBJECTS_DIR)/def $(file))</lef-files>),g' $(KLAYOUT_TECH_FILE) > $(OBJECTS_DIR)/klayout_wrap.lyt
211+
@mkdir -p $(dir $(OBJECTS_DIR)/klayout_wrap.lyt)
212+
$(PYTHON_EXE) $(UTILS_DIR)/generate_klayout_tech.py \
213+
--template $(KLAYOUT_TECH_FILE) \
214+
--output $(OBJECTS_DIR)/klayout_wrap.lyt \
215+
--lef-files $(OBJECTS_DIR)/klayout_tech.lef $(WRAP_LEFS) \
216+
--reference-dir $(OBJECTS_DIR)/def
213217

214218
$(WRAPPED_LEFS):
215219
mkdir -p $(OBJECTS_DIR)/lef $(OBJECTS_DIR)/def
@@ -594,8 +598,7 @@ klayout_guides: $(RESULTS_DIR)/5_route.def $(OBJECTS_DIR)/klayout.lyt
594598
.PHONY: finish
595599
finish: $(LOG_DIR)/6_report.log \
596600
$(RESULTS_DIR)/6_final.v \
597-
$(RESULTS_DIR)/6_final.sdc \
598-
$(GDS_FINAL_FILE)
601+
$(RESULTS_DIR)/6_final.sdc
599602
$(UNSET_AND_MAKE) elapsed
600603

601604
.PHONY: elapsed
@@ -627,7 +630,7 @@ final: finish
627630

628631
.PHONY: do-finish
629632
do-finish:
630-
$(UNSET_AND_MAKE) do-6_1_fill do-6_1_fill.sdc do-6_final.sdc do-6_report do-gds elapsed
633+
$(UNSET_AND_MAKE) do-6_1_fill do-6_1_fill.sdc do-6_final.sdc do-6_report elapsed
631634

632635
.PHONY: generate_abstract
633636
generate_abstract: $(RESULTS_DIR)/6_final.gds $(RESULTS_DIR)/6_final.def $(RESULTS_DIR)/6_final.v $(RESULTS_DIR)/6_final.sdc
@@ -643,6 +646,17 @@ do-generate_abstract:
643646
clean_abstract:
644647
rm -f $(RESULTS_DIR)/$(DESIGN_NAME).lib $(RESULTS_DIR)/$(DESIGN_NAME).lef
645648

649+
.PHONY: check-klayout
650+
check-klayout:
651+
@if [ -z "$(KLAYOUT_CMD)" ]; then \
652+
echo "Error: KLayout not found. Install KLayout or set KLAYOUT_CMD."; \
653+
echo "Hint: 'make finish' works without KLayout. Only GDS/DRC/LVS need it."; \
654+
exit 1; \
655+
fi
656+
657+
.PHONY: gds
658+
gds: $(GDS_FINAL_FILE)
659+
646660
# Merge wrapped macros using Klayout
647661
#-------------------------------------------------------------------------------
648662
$(WRAPPED_GDSOAS): $(OBJECTS_DIR)/klayout_wrap.lyt $(WRAPPED_LEFS)
@@ -658,7 +672,7 @@ $(WRAPPED_GDSOAS): $(OBJECTS_DIR)/klayout_wrap.lyt $(WRAPPED_LEFS)
658672

659673
# Merge GDS using Klayout
660674
#-------------------------------------------------------------------------------
661-
$(GDS_MERGED_FILE): $(RESULTS_DIR)/6_final.def $(OBJECTS_DIR)/klayout.lyt $(GDSOAS_FILES) $(WRAPPED_GDSOAS) $(SEAL_GDSOAS)
675+
$(GDS_MERGED_FILE): check-klayout $(RESULTS_DIR)/6_final.def $(OBJECTS_DIR)/klayout.lyt $(GDSOAS_FILES) $(WRAPPED_GDSOAS) $(SEAL_GDSOAS)
662676
$(UNSET_AND_MAKE) do-gds-merged
663677

664678
.PHONY: do-gds-merged
@@ -768,7 +782,7 @@ nuke: clean_test clean_issues
768782
# DEF/GDS/OAS viewer shortcuts
769783
#-------------------------------------------------------------------------------
770784
.PHONY: $(foreach file,$(RESULTS_DEF) $(RESULTS_GDS) $(RESULTS_OAS),klayout_$(file))
771-
$(foreach file,$(RESULTS_DEF) $(RESULTS_GDS) $(RESULTS_OAS),klayout_$(file)): klayout_%: $(OBJECTS_DIR)/klayout.lyt
785+
$(foreach file,$(RESULTS_DEF) $(RESULTS_GDS) $(RESULTS_OAS),klayout_$(file)): klayout_%: check-klayout $(OBJECTS_DIR)/klayout.lyt
772786
$(SCRIPTS_DIR)/klayout.sh -nn $(OBJECTS_DIR)/klayout.lyt $(RESULTS_DIR)/$*
773787

774788
$(eval $(call OPEN_GUI_SHORTCUT,synth,1_synth.odb))

flow/util/def2stream.py

Lines changed: 138 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -1,100 +1,151 @@
1-
import pya
1+
try:
2+
import pya
3+
except ImportError:
4+
pya = None
5+
26
import re
3-
import json
4-
import copy
57
import sys
68
import os
79

8-
errors = 0
9-
10-
# Load technology file
11-
tech = pya.Technology()
12-
tech.load(tech_file)
13-
layoutOptions = tech.load_layout_options
14-
if len(layer_map) > 0:
15-
layoutOptions.lefdef_config.map_file = layer_map
16-
17-
# Load def file
18-
main_layout = pya.Layout()
19-
print("[INFO] Reporting cells prior to loading DEF ...")
20-
for i in main_layout.each_cell():
21-
print("[INFO] '{0}'".format(i.name))
22-
23-
main_layout.read(in_def, layoutOptions)
24-
25-
# Clear cells
26-
top_cell_index = main_layout.cell(design_name).cell_index()
27-
28-
# remove orphan cell BUT preserve cell with VIA_
29-
# - KLayout is prepending VIA_ when reading DEF that instantiates LEF's via
30-
for i in main_layout.each_cell():
31-
if i.cell_index() != top_cell_index:
32-
if not i.name.startswith("VIA_") and not i.name.endswith("_DEF_FILL"):
33-
i.clear()
34-
35-
# Load in the gds to merge
36-
for fil in in_files.split():
37-
print("\t{0}".format(fil))
38-
main_layout.read(fil)
39-
40-
# Copy the top level only to a new layout
41-
top_only_layout = pya.Layout()
42-
top_only_layout.dbu = main_layout.dbu
43-
top = top_only_layout.create_cell(design_name)
44-
top.copy_tree(main_layout.cell(design_name))
45-
46-
missing_cell = False
47-
allow_empty = os.environ.get("GDS_ALLOW_EMPTY", "")
48-
regex = re.compile(allow_empty) if allow_empty else None
49-
50-
if allow_empty:
51-
print(f"[INFO] GDS_ALLOW_EMPTY={allow_empty}")
52-
53-
for i in top_only_layout.each_cell():
54-
if i.is_empty():
55-
missing_cell = True
56-
if regex is not None and regex.match(i.name):
57-
print(
58-
"[WARNING] LEF Cell '{0}' ignored. Matches GDS_ALLOW_EMPTY.".format(
59-
i.name
60-
)
61-
)
62-
else:
63-
print(
64-
"[ERROR] LEF Cell '{0}' has no matching GDS/OAS cell."
65-
" Cell will be empty.".format(i.name)
66-
)
67-
errors += 1
6810

69-
if not missing_cell:
70-
print("[INFO] All LEF cells have matching GDS/OAS cells")
11+
def merge_gds(
12+
pya_mod,
13+
tech_file,
14+
layer_map,
15+
in_def,
16+
design_name,
17+
in_files,
18+
seal_file,
19+
out_file,
20+
allow_empty="",
21+
):
22+
"""Merge DEF and GDS/OAS files into a single stream file.
23+
24+
Args:
25+
pya_mod: The pya module (klayout Python API).
26+
tech_file: Path to klayout technology file.
27+
layer_map: Path to layer map file (empty string if none).
28+
in_def: Path to input DEF file.
29+
design_name: Top-level design name.
30+
in_files: Space-separated string of GDS/OAS files to merge.
31+
seal_file: Path to seal ring GDS/OAS file (empty string if none).
32+
out_file: Path to output GDS/OAS file.
33+
allow_empty: Regex pattern for cells allowed to be empty.
34+
35+
Returns:
36+
Number of errors encountered.
37+
"""
38+
errors = 0
39+
40+
# Load technology file
41+
tech = pya_mod.Technology()
42+
tech.load(tech_file)
43+
layout_options = tech.load_layout_options
44+
if len(layer_map) > 0:
45+
layout_options.lefdef_config.map_file = layer_map
46+
47+
# Load def file
48+
main_layout = pya_mod.Layout()
49+
print("[INFO] Reporting cells prior to loading DEF ...")
50+
for i in main_layout.each_cell():
51+
print("[INFO] '{0}'".format(i.name))
52+
53+
main_layout.read(in_def, layout_options)
54+
55+
# Clear cells
56+
top_cell_index = main_layout.cell(design_name).cell_index()
57+
58+
# remove orphan cell BUT preserve cell with VIA_
59+
# - KLayout is prepending VIA_ when reading DEF that instantiates LEF's via
60+
for i in main_layout.each_cell():
61+
if i.cell_index() != top_cell_index:
62+
if not i.name.startswith("VIA_") and not i.name.endswith("_DEF_FILL"):
63+
i.clear()
64+
65+
# Load in the gds to merge
66+
for fil in in_files.split():
67+
print("\t{0}".format(fil))
68+
main_layout.read(fil)
69+
70+
# Copy the top level only to a new layout
71+
top_only_layout = pya_mod.Layout()
72+
top_only_layout.dbu = main_layout.dbu
73+
top = top_only_layout.create_cell(design_name)
74+
top.copy_tree(main_layout.cell(design_name))
75+
76+
missing_cell = False
77+
regex = re.compile(allow_empty) if allow_empty else None
78+
79+
if allow_empty:
80+
print(f"[INFO] GDS_ALLOW_EMPTY={allow_empty}")
81+
82+
for i in top_only_layout.each_cell():
83+
if i.is_empty():
84+
missing_cell = True
85+
if regex is not None and regex.match(i.name):
86+
print(
87+
"[WARNING] LEF Cell '{0}' ignored. Matches GDS_ALLOW_EMPTY.".format(
88+
i.name
89+
)
90+
)
91+
else:
92+
print(
93+
"[ERROR] LEF Cell '{0}' has no matching GDS/OAS cell."
94+
" Cell will be empty.".format(i.name)
95+
)
96+
errors += 1
7197

72-
orphan_cell = False
73-
for i in top_only_layout.each_cell():
74-
if i.name != design_name and i.parent_cells() == 0:
75-
orphan_cell = True
76-
print("[ERROR] Found orphan cell '{0}'".format(i.name))
77-
errors += 1
98+
if not missing_cell:
99+
print("[INFO] All LEF cells have matching GDS/OAS cells")
78100

79-
if not orphan_cell:
80-
print("[INFO] No orphan cells in the final layout")
101+
orphan_cell = False
102+
for i in top_only_layout.each_cell():
103+
if i.name != design_name and i.parent_cells() == 0:
104+
orphan_cell = True
105+
print("[ERROR] Found orphan cell '{0}'".format(i.name))
106+
errors += 1
81107

108+
if not orphan_cell:
109+
print("[INFO] No orphan cells in the final layout")
82110

83-
if seal_file:
84-
top_cell = top_only_layout.top_cell()
111+
if seal_file:
112+
top_cell = top_only_layout.top_cell()
85113

86-
top_only_layout.read(seal_file)
114+
top_only_layout.read(seal_file)
87115

88-
for cell in top_only_layout.top_cells():
89-
if cell != top_cell:
90-
print(
91-
"[INFO] Merging '{0}' as child of '{1}'".format(
92-
cell.name, top_cell.name
116+
for cell in top_only_layout.top_cells():
117+
if cell != top_cell:
118+
print(
119+
"[INFO] Merging '{0}' as child of '{1}'".format(
120+
cell.name, top_cell.name
121+
)
93122
)
123+
top.insert(pya_mod.CellInstArray(cell.cell_index(), pya_mod.Trans()))
124+
125+
# Write out the GDS
126+
top_only_layout.write(out_file)
127+
128+
return errors
129+
130+
131+
# When run via klayout -r, globals tech_file, layer_map, in_def, etc.
132+
# are set by klayout's -rd mechanism.
133+
if pya is not None:
134+
try:
135+
# These globals are set by klayout -rd flags
136+
sys.exit(
137+
merge_gds(
138+
pya_mod=pya,
139+
tech_file=tech_file, # noqa: F821 - set by klayout -rd
140+
layer_map=layer_map, # noqa: F821
141+
in_def=in_def, # noqa: F821
142+
design_name=design_name, # noqa: F821
143+
in_files=in_files, # noqa: F821
144+
seal_file=seal_file, # noqa: F821
145+
out_file=out_file, # noqa: F821
146+
allow_empty=os.environ.get("GDS_ALLOW_EMPTY", ""),
94147
)
95-
top.insert(pya.CellInstArray(cell.cell_index(), pya.Trans()))
96-
97-
# Write out the GDS
98-
top_only_layout.write(out_file)
99-
100-
sys.exit(errors)
148+
)
149+
except NameError:
150+
# Not running under klayout -r, pya available but no -rd globals
151+
pass

0 commit comments

Comments
 (0)