From 6ab8b3145a8a95763a5674af22a4e0fc5df2b196 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Harboe?= Date: Wed, 21 Oct 2026 06:54:54 +0200 Subject: [PATCH] klayout.sh: without KLayout, stream out an empty GDS file and warn MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Quality of life for those who don't need or want a GDS file. KLayout is a big application, packaged differently on every distribution, and the flow needs it for the small task of writing out a GDS file at the very end. Without it, `make` stopped there: Error: KLayout not found. Install KLayout or set KLAYOUT_CMD. Now klayout.sh, when KLAYOUT_CMD is empty and the call is the stream-out (def2stream.py with an out_file), writes an empty file and says so: WARNING: klayout not found, mocking klayout by writing empty file and the flow runs to the end. Installing KLayout opts out. Nobody mistakes an empty GDS file for a valid one, and the warning says why it is empty. Every other KLayout call, DRC, LVS, the gallery and the viewers, still stops with the error. No Makefile conditionals: the decision is made in klayout.sh, at the one call that writes the GDS file. The GDS merge no longer depends on check-klayout. DRC and LVS were guarded by check-klayout only through the merge they depend on, so they get it directly: `make drc` without KLayout stops with the same error as before. The hint under that error names what still needs KLayout. A hierarchical design works the same way: each block's GDS is written empty, and so is the parent's merge of them. flow/test/test_klayout_sh.py checks the stream-out writes an empty file and warns, DRC and the viewer still stop without KLayout, and an installed KLayout gets the call unchanged. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Øyvind Harboe --- flow/Makefile | 8 +-- flow/docs/KLayoutOptionalDependency.md | 26 ++++++-- flow/scripts/klayout.sh | 21 +++++++ flow/test/test_klayout_sh.py | 83 ++++++++++++++++++++++++++ 4 files changed, 128 insertions(+), 10 deletions(-) create mode 100644 flow/test/test_klayout_sh.py diff --git a/flow/Makefile b/flow/Makefile index 759e13578d..cc4d335769 100644 --- a/flow/Makefile +++ b/flow/Makefile @@ -724,7 +724,7 @@ clean_abstract: check-klayout: @if [ -z "$(KLAYOUT_CMD)" ]; then \ echo "Error: KLayout not found. Install KLayout or set KLAYOUT_CMD."; \ - echo "Hint: KLayout is needed for GDS/DRC/LVS targets."; \ + echo "Hint: KLayout is needed for DRC, LVS, the gallery and the viewers."; \ exit 1; \ fi @@ -747,7 +747,7 @@ $(WRAPPED_GDSOAS): $(OBJECTS_DIR)/klayout_wrap.lyt $(WRAPPED_LEFS) # Merge GDS using Klayout #------------------------------------------------------------------------------- -$(GDS_MERGED_FILE): $(RESULTS_DIR)/6_final.def $(OBJECTS_DIR)/klayout.lyt $(GDSOAS_FILES) $(WRAPPED_GDSOAS) $(SEAL_GDSOAS) | check-klayout +$(GDS_MERGED_FILE): $(RESULTS_DIR)/6_final.def $(OBJECTS_DIR)/klayout.lyt $(GDSOAS_FILES) $(WRAPPED_GDSOAS) $(SEAL_GDSOAS) $(UNSET_AND_MAKE) do-gds-merged .PHONY: do-gds-merged @@ -775,7 +775,7 @@ $(GDS_FINAL_FILE): $(GDS_MERGED_FILE) .PHONY: drc drc: $(REPORTS_DIR)/6_drc.lyrdb -$(REPORTS_DIR)/6_drc.lyrdb: $(GDS_FINAL_FILE) $(KLAYOUT_DRC_FILE) +$(REPORTS_DIR)/6_drc.lyrdb: $(GDS_FINAL_FILE) $(KLAYOUT_DRC_FILE) | check-klayout ifneq ($(KLAYOUT_DRC_FILE),) $(RUN_CMD) --log $(abspath $(LOG_DIR)/6_drc.log) --tee -- \ $(SCRIPTS_DIR)/klayout.sh -zz -rd in_gds="$<" \ @@ -796,7 +796,7 @@ $(OBJECTS_DIR)/6_final_concat.cdl: $(RESULTS_DIR)/6_final.cdl $(CDL_FILE) .PHONY: lvs lvs: $(RESULTS_DIR)/6_lvs.lvsdb -$(RESULTS_DIR)/6_lvs.lvsdb: $(GDS_FINAL_FILE) $(KLAYOUT_LVS_FILE) $(OBJECTS_DIR)/6_final_concat.cdl +$(RESULTS_DIR)/6_lvs.lvsdb: $(GDS_FINAL_FILE) $(KLAYOUT_LVS_FILE) $(OBJECTS_DIR)/6_final_concat.cdl | check-klayout ifneq ($(KLAYOUT_LVS_FILE),) $(RUN_CMD) --log $(abspath $(LOG_DIR)/6_lvs.log) --tee -- \ $(SCRIPTS_DIR)/klayout.sh -b -rd in_gds="$<" \ diff --git a/flow/docs/KLayoutOptionalDependency.md b/flow/docs/KLayoutOptionalDependency.md index c469160a27..af838b5aac 100644 --- a/flow/docs/KLayoutOptionalDependency.md +++ b/flow/docs/KLayoutOptionalDependency.md @@ -1,16 +1,30 @@ # KLayout as an Optional Dependency -KLayout is only required for GDS/OAS stream generation, DRC, and LVS +KLayout is only required for a real GDS/OAS stream, DRC, and LVS verification. All other ORFS functionality — synthesis, floorplanning, placement, CTS, routing, timing reports, and abstract generation — works without KLayout installed. +KLayout is a big application, packaged differently on every +distribution, and writing out a GDS file is a small task at the end of +the flow. Many who run ORFS don't need or want a GDS file. Without +KLayout, `make` therefore streams out an empty file and warns: + +``` +WARNING: klayout not found, mocking klayout by writing empty file ./results/asap7/gcd/base/6_1_merged.gds +``` + +Nobody mistakes an empty GDS file for a valid one. Installing KLayout +opts out: the GDS file is then written by KLayout. A GDS file written +empty before KLayout was installed is up to date as far as make knows; +`make clean_finish` removes it. + ## Makefile Targets | Target | Requires KLayout | Description | |---|---|---| -| `make finish` | Yes | Complete flow including GDS generation | -| `make gds` | Yes | Generate GDS/OAS from finished design | +| `make finish` | No (empty GDS without it) | Complete flow including GDS generation | +| `make gds` | No (empty GDS without it) | Generate GDS/OAS from finished design | | `make drc` | Yes | Run DRC checks (requires GDS) | | `make lvs` | Yes | Run LVS checks (requires GDS) | | `make gallery` | Yes | Generate layout screenshots | @@ -18,11 +32,11 @@ without KLayout installed. | `make generate_abstract` | No | Generate LEF/LIB abstracts | A `check-klayout` guard produces a clear error message when KLayout is -missing and a KLayout-dependent target is invoked: +missing and a target that needs it is invoked: ``` Error: KLayout not found. Install KLayout or set KLAYOUT_CMD. -Hint: KLayout is needed for GDS/DRC/LVS targets. +Hint: KLayout is needed for DRC, LVS, the gallery and the viewers. ``` ## bazel-orfs Integration @@ -30,7 +44,7 @@ Hint: KLayout is needed for GDS/DRC/LVS targets. bazel-orfs uses the `do-` prefixed targets which bypass Make's dependency management. `do-finish` / `do-final` only run the finish stage recipe itself, while `make finish` also pulls in the GDS target as a Make -dependency. `do-gds` runs GDS generation separately (requires KLayout). +dependency. `do-gds` runs GDS generation separately (an empty file without KLayout). An `orfs_gds()` Bazel rule can call `do-gds` independently from `orfs_flow()`, making KLayout an optional toolchain dependency configured diff --git a/flow/scripts/klayout.sh b/flow/scripts/klayout.sh index 3efd50f5d1..af53728796 100755 --- a/flow/scripts/klayout.sh +++ b/flow/scripts/klayout.sh @@ -1,6 +1,27 @@ #!/usr/bin/env bash set -u -eo pipefail if [ -z "$KLAYOUT_CMD" ]; then + # Quality of life for those who don't need or want a GDS file: KLayout + # is a big, distribution-specific dependency for the small task of + # writing one out. Without it, streaming out GDS/OAS writes an empty + # file and warns, so the flow runs to the end. Installing KLayout opts + # out. Nobody mistakes an empty GDS file for a valid one; DRC, LVS and + # the viewers still need KLayout and stop here. + out_file="" + stream=0 + prev="" + for arg in "$@"; do + case "$prev,$arg" in + -rd,out_file=*) out_file="${arg#out_file=}" ;; + -r,*/def2stream.py) stream=1 ;; + esac + prev="$arg" + done + if [ "$stream" = 1 ] && [ -n "$out_file" ]; then + echo "WARNING: klayout not found, mocking klayout by writing empty file $out_file" + : >"$out_file" + exit 0 + fi echo "Error: KLayout not found in PATH" exit 1 fi diff --git a/flow/test/test_klayout_sh.py b/flow/test/test_klayout_sh.py new file mode 100644 index 0000000000..e4cbe8efe5 --- /dev/null +++ b/flow/test/test_klayout_sh.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python3 +"""klayout.sh without KLayout: streaming out GDS writes an empty file and +warns, so the flow runs to the end for those who don't need or want a GDS +file. Every other KLayout call, DRC and LVS among them, still stops with +an error. With KLayout, the call goes to it unchanged. +""" + +import os +import subprocess +import tempfile +import unittest + +SCRIPTS_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "scripts") +KLAYOUT_SH = os.path.join(SCRIPTS_DIR, "klayout.sh") + + +class TestKlayoutSh(unittest.TestCase): + def setUp(self): + self.tmp_dir = tempfile.TemporaryDirectory() + self.out_file = os.path.join(self.tmp_dir.name, "6_1_merged.gds") + + def tearDown(self): + self.tmp_dir.cleanup() + + def run_klayout(self, klayout_cmd, *args): + return subprocess.run( + ["bash", KLAYOUT_SH, *args], + env=dict(os.environ, KLAYOUT_CMD=klayout_cmd), + capture_output=True, + text=True, + ) + + def stream_args(self): + return [ + "-zz", + "-rd", + "design_name=gcd", + "-rd", + f"out_file={self.out_file}", + "-r", + "/flow/util/def2stream.py", + ] + + def test_stream_out_writes_an_empty_file_and_warns(self): + result = self.run_klayout("", *self.stream_args()) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertIn( + "WARNING: klayout not found, mocking klayout by writing empty file " + + self.out_file, + result.stdout, + ) + self.assertEqual(os.path.getsize(self.out_file), 0) + + def test_drc_still_needs_klayout(self): + result = self.run_klayout( + "", + "-zz", + "-rd", + "in_gds=6_final.gds", + "-rd", + "report_file=6_drc.lyrdb", + "-r", + "/flow/platforms/asap7/drc/asap7.lydrc", + ) + self.assertEqual(result.returncode, 1) + self.assertIn("Error: KLayout not found", result.stdout) + + def test_viewer_still_needs_klayout(self): + result = self.run_klayout("", "-nn", "klayout.lyt", "6_final.odb") + self.assertEqual(result.returncode, 1) + self.assertIn("Error: KLayout not found", result.stdout) + self.assertFalse(os.path.exists(self.out_file)) + + def test_installed_klayout_gets_the_call(self): + result = self.run_klayout("echo", *self.stream_args()) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertNotIn("WARNING", result.stdout) + self.assertIn(f"out_file={self.out_file}", result.stdout) + self.assertFalse(os.path.exists(self.out_file)) + + +if __name__ == "__main__": + unittest.main()