From 67175cd65051f43205ce42851058e81f7b1649e8 Mon Sep 17 00:00:00 2001 From: Serge Rabyking Date: Wed, 29 Apr 2026 08:35:01 +0200 Subject: [PATCH 1/8] feat(submit): pack rtlil + pins.lock into a single bundle.zip The submit endpoint receives one multipart 'bundle' part instead of separate 'rtlil' and 'config' parts. Inside the zip: manifest.json {"version": "1", "rtlil": "...", "config": "pins.lock"} e.g. top.il, taken from rtlil_path pins.lock the pinlock JSON The manifest is the only contract; consumers locate the rtlil and config payloads through it. This lays the groundwork for adding macro folders later by extending the manifest, with no further wire-format change. --- chipflow/platform/silicon_step.py | 48 +++++++++++++--- tests/test_silicon_submit.py | 96 +++++++++++++++++++++++++++---- 2 files changed, 125 insertions(+), 19 deletions(-) diff --git a/chipflow/platform/silicon_step.py b/chipflow/platform/silicon_step.py index 8ab5c354..74330448 100644 --- a/chipflow/platform/silicon_step.py +++ b/chipflow/platform/silicon_step.py @@ -3,15 +3,17 @@ # SPDX-License-Identifier: BSD-2-Clause import inspect +import io import json import logging import os import requests -import shutil import subprocess import sys import urllib3 import webbrowser +import zipfile +from pathlib import Path from pprint import pformat @@ -30,6 +32,37 @@ logger = logging.getLogger(__name__) +def _build_bundle_zip(rtlil_path, config: str) -> bytes: + """Pack the submission into a single zip with a manifest. + + Layout:: + + manifest.json + # e.g. "top.il", taken from rtlil_path + pins.lock # the pinlock JSON + + The manifest is the only contract: consumers locate the rtlil and + config payloads via ``manifest["rtlil"]`` and ``manifest["config"]``. + Future additions (e.g. macro folders) extend the manifest without + changing this function's signature on the wire. + """ + rtlil_arc = Path(rtlil_path).name + config_arc = "pins.lock" + manifest = { + "version": "1", + "rtlil": rtlil_arc, + "config": config_arc, + } + manifest_bytes = (json.dumps(manifest, indent=2) + "\n").encode("utf-8") + + buf = io.BytesIO() + with zipfile.ZipFile(buf, mode="w", compression=zipfile.ZIP_DEFLATED) as zf: + zf.writestr("manifest.json", manifest_bytes) + zf.writestr(config_arc, config) + zf.write(str(rtlil_path), arcname=rtlil_arc) + return buf.getvalue() + + def halo_logging(closure): class ClosureStreamHandler(logging.StreamHandler): def emit(self, record): @@ -181,16 +214,14 @@ def submit(self, rtlil_path, args): pinlock = load_pinlock() config = pinlock.model_dump_json(indent=2) + bundle_bytes = _build_bundle_zip(rtlil_path, config) + if args.dry_run: sp.succeed(f"✅ Design `{data['projectId']}:{data['name']}` ready for submission to ChipFlow cloud!") logger.debug(f"data=\n{json.dumps(data, indent=2)}") logger.debug(f"files['config']=\n{config}") - shutil.copyfile(rtlil_path, 'rtlil') - with open("rtlil", 'w') as f: - json.dump(data, f) - with open("config", 'w') as f: - f.write(config) - sp.info("Compiled design and configuration can be found in in `rtlil` and `config`") + Path("bundle.zip").write_bytes(bundle_bytes) + sp.info("Compiled submission written to `bundle.zip` (manifest.json + rtlil + pins.lock)") return def network_err(e): @@ -217,8 +248,7 @@ def network_err(e): auth=("", self._chipflow_api_key), data=data, files={ - "rtlil": open(rtlil_path, "rb"), - "config": config, + "bundle": ("bundle.zip", bundle_bytes, "application/zip"), }, allow_redirects=False ) diff --git a/tests/test_silicon_submit.py b/tests/test_silicon_submit.py index 83c10a18..ec0542df 100644 --- a/tests/test_silicon_submit.py +++ b/tests/test_silicon_submit.py @@ -1,13 +1,16 @@ # SPDX-License-Identifier: BSD-2-Clause +import io +import json import unittest +import zipfile from unittest import mock from argparse import Namespace from pathlib import Path import tempfile import os -from chipflow.platform.silicon_step import SiliconStep +from chipflow.platform.silicon_step import SiliconStep, _build_bundle_zip class TestSiliconSubmitBrowserPrompt(unittest.TestCase): @@ -54,9 +57,9 @@ def test_browser_prompt_yes(self, mock_subprocess, mock_isatty, mock_input, mock step.platform._ports = {} # Mock the submit method dependencies - with mock.patch.object(step, 'prepare', return_value='/tmp/test.il'): - with mock.patch('builtins.open', mock.mock_open(read_data=b'')): - with mock.patch('chipflow.platform.silicon_step.requests.post') as mock_post: + with mock.patch.object(step, 'prepare', return_value='/tmp/test.il'), \ + mock.patch('chipflow.platform.silicon_step._build_bundle_zip', return_value=b'fake-bundle'): + with mock.patch('chipflow.platform.silicon_step.requests.post') as mock_post: # Mock successful submission mock_response = mock.MagicMock() mock_response.status_code = 200 @@ -103,9 +106,9 @@ def test_browser_prompt_no(self, mock_subprocess, mock_isatty, mock_input, mock_ step.platform._ports = {} # Mock the submit method dependencies - with mock.patch.object(step, 'prepare', return_value='/tmp/test.il'): - with mock.patch('builtins.open', mock.mock_open(read_data=b'')): - with mock.patch('chipflow.platform.silicon_step.requests.post') as mock_post: + with mock.patch.object(step, 'prepare', return_value='/tmp/test.il'), \ + mock.patch('chipflow.platform.silicon_step._build_bundle_zip', return_value=b'fake-bundle'): + with mock.patch('chipflow.platform.silicon_step.requests.post') as mock_post: # Mock successful submission mock_response = mock.MagicMock() mock_response.status_code = 200 @@ -150,9 +153,9 @@ def test_browser_prompt_not_tty(self, mock_subprocess, mock_isatty, mock_input, step.platform._ports = {} # Mock the submit method dependencies - with mock.patch.object(step, 'prepare', return_value='/tmp/test.il'): - with mock.patch('builtins.open', mock.mock_open(read_data=b'')): - with mock.patch('chipflow.platform.silicon_step.requests.post') as mock_post: + with mock.patch.object(step, 'prepare', return_value='/tmp/test.il'), \ + mock.patch('chipflow.platform.silicon_step._build_bundle_zip', return_value=b'fake-bundle'): + with mock.patch('chipflow.platform.silicon_step.requests.post') as mock_post: # Mock successful submission mock_response = mock.MagicMock() mock_response.status_code = 200 @@ -175,5 +178,78 @@ def test_browser_prompt_not_tty(self, mock_subprocess, mock_isatty, mock_input, mock_webbrowser.assert_not_called() +class TestBuildBundleZip(unittest.TestCase): + """Tests for the _build_bundle_zip helper.""" + + def test_manifest_and_layout(self): + with tempfile.TemporaryDirectory() as td: + rtlil_path = Path(td) / "top.il" + rtlil_path.write_text("module top(); endmodule\n") + config = '{"pins": []}' + + blob = _build_bundle_zip(rtlil_path, config) + + with zipfile.ZipFile(io.BytesIO(blob)) as zf: + names = set(zf.namelist()) + self.assertEqual(names, {"manifest.json", "top.il", "pins.lock"}) + + manifest = json.loads(zf.read("manifest.json")) + self.assertEqual(manifest["version"], "1") + self.assertEqual(manifest["rtlil"], "top.il") + self.assertEqual(manifest["config"], "pins.lock") + + self.assertEqual(zf.read("top.il").decode(), "module top(); endmodule\n") + self.assertEqual(zf.read("pins.lock").decode(), config) + + def test_uses_real_rtlil_filename(self): + """Bundle preserves the source rtlil filename (not a fixed string).""" + with tempfile.TemporaryDirectory() as td: + rtlil_path = Path(td) / "weird_name.rtlil" + rtlil_path.write_text("x") + blob = _build_bundle_zip(rtlil_path, "{}") + with zipfile.ZipFile(io.BytesIO(blob)) as zf: + self.assertIn("weird_name.rtlil", zf.namelist()) + manifest = json.loads(zf.read("manifest.json")) + self.assertEqual(manifest["rtlil"], "weird_name.rtlil") + + +class TestSiliconSubmitBundlePost(unittest.TestCase): + """The submit() path posts a single 'bundle' multipart part.""" + + @mock.patch('chipflow.packaging.load_pinlock') + @mock.patch('chipflow.platform.silicon_step.subprocess.check_output') + def test_submit_sends_single_bundle_part(self, mock_subprocess, mock_load_pinlock): + mock_subprocess.return_value = 'test123\n' + mock_pinlock = mock.MagicMock() + mock_pinlock.model_dump_json.return_value = '{}' + mock_load_pinlock.return_value = mock_pinlock + + with mock.patch('chipflow.platform.silicon_step.SiliconPlatform'): + config = mock.MagicMock() + config.chipflow.silicon = True + config.chipflow.project_name = 'test_project' + step = SiliconStep(config) + step.platform._ports = {} + + with mock.patch.object(step, 'prepare', return_value='/tmp/test.il'), \ + mock.patch('chipflow.platform.silicon_step._build_bundle_zip', + return_value=b'fake-bundle-bytes'), \ + mock.patch('chipflow.platform.silicon_step.requests.post') as mock_post, \ + mock.patch('chipflow.platform.silicon_step.get_api_key', return_value='k'), \ + mock.patch('chipflow.platform.silicon_step.exit'), \ + mock.patch('sys.stdout.isatty', return_value=False): + mock_post.return_value = mock.MagicMock( + status_code=200, json=lambda: {'build_id': 'b1'}) + step._chipflow_api_key = 'k' + step.submit('/tmp/test.il', Namespace(dry_run=False, wait=False)) + + files = mock_post.call_args.kwargs["files"] + self.assertEqual(set(files.keys()), {"bundle"}) + filename, payload, content_type = files["bundle"] + self.assertEqual(filename, "bundle.zip") + self.assertEqual(payload, b'fake-bundle-bytes') + self.assertEqual(content_type, "application/zip") + + if __name__ == "__main__": unittest.main() From 8e38c68df24a2998874fc76fa8efdcca2f068c11 Mon Sep 17 00:00:00 2001 From: Serge Rabyking Date: Wed, 29 Apr 2026 08:48:31 +0200 Subject: [PATCH 2/8] feat(submit): include project name in bundle manifest Adds the chipflow.toml [chipflow] project_name to manifest.json under a "project" key so the backend can identify the design without parsing the config payload (useful for log lines, working-directory naming, and dashboards). --- chipflow/platform/silicon_step.py | 10 ++++++++-- tests/test_silicon_submit.py | 5 +++-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/chipflow/platform/silicon_step.py b/chipflow/platform/silicon_step.py index 74330448..ad50f119 100644 --- a/chipflow/platform/silicon_step.py +++ b/chipflow/platform/silicon_step.py @@ -32,7 +32,7 @@ logger = logging.getLogger(__name__) -def _build_bundle_zip(rtlil_path, config: str) -> bytes: +def _build_bundle_zip(rtlil_path, config: str, project_name: str) -> bytes: """Pack the submission into a single zip with a manifest. Layout:: @@ -41,6 +41,10 @@ def _build_bundle_zip(rtlil_path, config: str) -> bytes: # e.g. "top.il", taken from rtlil_path pins.lock # the pinlock JSON + ``project_name`` is the chipflow.toml ``[chipflow] project_name`` value; + consumers (logs, dashboards, the backend's working directory naming) + use it to identify the design without re-parsing the config. + The manifest is the only contract: consumers locate the rtlil and config payloads via ``manifest["rtlil"]`` and ``manifest["config"]``. Future additions (e.g. macro folders) extend the manifest without @@ -50,6 +54,7 @@ def _build_bundle_zip(rtlil_path, config: str) -> bytes: config_arc = "pins.lock" manifest = { "version": "1", + "project": project_name, "rtlil": rtlil_arc, "config": config_arc, } @@ -214,7 +219,8 @@ def submit(self, rtlil_path, args): pinlock = load_pinlock() config = pinlock.model_dump_json(indent=2) - bundle_bytes = _build_bundle_zip(rtlil_path, config) + bundle_bytes = _build_bundle_zip( + rtlil_path, config, self.config.chipflow.project_name) if args.dry_run: sp.succeed(f"✅ Design `{data['projectId']}:{data['name']}` ready for submission to ChipFlow cloud!") diff --git a/tests/test_silicon_submit.py b/tests/test_silicon_submit.py index ec0542df..8fc6b2e2 100644 --- a/tests/test_silicon_submit.py +++ b/tests/test_silicon_submit.py @@ -187,7 +187,7 @@ def test_manifest_and_layout(self): rtlil_path.write_text("module top(); endmodule\n") config = '{"pins": []}' - blob = _build_bundle_zip(rtlil_path, config) + blob = _build_bundle_zip(rtlil_path, config, "my_project") with zipfile.ZipFile(io.BytesIO(blob)) as zf: names = set(zf.namelist()) @@ -195,6 +195,7 @@ def test_manifest_and_layout(self): manifest = json.loads(zf.read("manifest.json")) self.assertEqual(manifest["version"], "1") + self.assertEqual(manifest["project"], "my_project") self.assertEqual(manifest["rtlil"], "top.il") self.assertEqual(manifest["config"], "pins.lock") @@ -206,7 +207,7 @@ def test_uses_real_rtlil_filename(self): with tempfile.TemporaryDirectory() as td: rtlil_path = Path(td) / "weird_name.rtlil" rtlil_path.write_text("x") - blob = _build_bundle_zip(rtlil_path, "{}") + blob = _build_bundle_zip(rtlil_path, "{}", "p") with zipfile.ZipFile(io.BytesIO(blob)) as zf: self.assertIn("weird_name.rtlil", zf.namelist()) manifest = json.loads(zf.read("manifest.json")) From 18d7427a93d15b75350c7f94d00373b0b4bd642b Mon Sep 17 00:00:00 2001 From: Serge Rabyking Date: Wed, 29 Apr 2026 08:59:45 +0200 Subject: [PATCH 3/8] fix(submit): write dry-run bundle.zip next to the rtlil Drops the artifact next to amaranth's build output (Path(rtlil_path).parent) instead of the current working directory, so it lands inside the project's build folder alongside the rtlil it was packed from. --- chipflow/platform/silicon_step.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/chipflow/platform/silicon_step.py b/chipflow/platform/silicon_step.py index ad50f119..828e7002 100644 --- a/chipflow/platform/silicon_step.py +++ b/chipflow/platform/silicon_step.py @@ -226,8 +226,9 @@ def submit(self, rtlil_path, args): sp.succeed(f"✅ Design `{data['projectId']}:{data['name']}` ready for submission to ChipFlow cloud!") logger.debug(f"data=\n{json.dumps(data, indent=2)}") logger.debug(f"files['config']=\n{config}") - Path("bundle.zip").write_bytes(bundle_bytes) - sp.info("Compiled submission written to `bundle.zip` (manifest.json + rtlil + pins.lock)") + bundle_path = Path(rtlil_path).parent / "bundle.zip" + bundle_path.write_bytes(bundle_bytes) + sp.info(f"Compiled submission written to `{bundle_path}` (manifest.json + rtlil + pins.lock)") return def network_err(e): From b8f154f65fb9eb907d71401a4bf5f7170fc3c6dc Mon Sep 17 00:00:00 2001 From: Serge Rabyking Date: Wed, 29 Apr 2026 09:24:43 +0200 Subject: [PATCH 4/8] refactor(submit): rename manifest "config" key to "pins_lock" The key now matches what the value actually points to (pins.lock). Leaves room for adding other config-like files later without overloading a generic "config" key. --- chipflow/platform/silicon_step.py | 10 +++++----- tests/test_silicon_submit.py | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/chipflow/platform/silicon_step.py b/chipflow/platform/silicon_step.py index 828e7002..d8275ffe 100644 --- a/chipflow/platform/silicon_step.py +++ b/chipflow/platform/silicon_step.py @@ -43,27 +43,27 @@ def _build_bundle_zip(rtlil_path, config: str, project_name: str) -> bytes: ``project_name`` is the chipflow.toml ``[chipflow] project_name`` value; consumers (logs, dashboards, the backend's working directory naming) - use it to identify the design without re-parsing the config. + use it to identify the design without re-parsing the pinlock. The manifest is the only contract: consumers locate the rtlil and - config payloads via ``manifest["rtlil"]`` and ``manifest["config"]``. + pinlock payloads via ``manifest["rtlil"]`` and ``manifest["pins_lock"]``. Future additions (e.g. macro folders) extend the manifest without changing this function's signature on the wire. """ rtlil_arc = Path(rtlil_path).name - config_arc = "pins.lock" + pins_lock_arc = "pins.lock" manifest = { "version": "1", "project": project_name, "rtlil": rtlil_arc, - "config": config_arc, + "pins_lock": pins_lock_arc, } manifest_bytes = (json.dumps(manifest, indent=2) + "\n").encode("utf-8") buf = io.BytesIO() with zipfile.ZipFile(buf, mode="w", compression=zipfile.ZIP_DEFLATED) as zf: zf.writestr("manifest.json", manifest_bytes) - zf.writestr(config_arc, config) + zf.writestr(pins_lock_arc, config) zf.write(str(rtlil_path), arcname=rtlil_arc) return buf.getvalue() diff --git a/tests/test_silicon_submit.py b/tests/test_silicon_submit.py index 8fc6b2e2..922a4793 100644 --- a/tests/test_silicon_submit.py +++ b/tests/test_silicon_submit.py @@ -197,7 +197,7 @@ def test_manifest_and_layout(self): self.assertEqual(manifest["version"], "1") self.assertEqual(manifest["project"], "my_project") self.assertEqual(manifest["rtlil"], "top.il") - self.assertEqual(manifest["config"], "pins.lock") + self.assertEqual(manifest["pins_lock"], "pins.lock") self.assertEqual(zf.read("top.il").decode(), "module top(); endmodule\n") self.assertEqual(zf.read("pins.lock").decode(), config) From 989773a4c39c1f0f7b9dfeaa409fc85f813998c1 Mon Sep 17 00:00:00 2001 From: Serge Rabyking Date: Wed, 29 Apr 2026 11:22:36 +0200 Subject: [PATCH 5/8] refactor(submit): suffix file-pointer manifest keys with _file Renames manifest keys whose value names a file in the archive: "rtlil" -> "rtlil_file", "pins_lock" -> "pins_lock_file". Plain value keys ("version", "project") stay as-is. Makes the role of each key self-documenting and unambiguous as the manifest grows. --- chipflow/platform/silicon_step.py | 13 ++++++++----- tests/test_silicon_submit.py | 6 +++--- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/chipflow/platform/silicon_step.py b/chipflow/platform/silicon_step.py index d8275ffe..2982f28f 100644 --- a/chipflow/platform/silicon_step.py +++ b/chipflow/platform/silicon_step.py @@ -46,17 +46,20 @@ def _build_bundle_zip(rtlil_path, config: str, project_name: str) -> bytes: use it to identify the design without re-parsing the pinlock. The manifest is the only contract: consumers locate the rtlil and - pinlock payloads via ``manifest["rtlil"]`` and ``manifest["pins_lock"]``. - Future additions (e.g. macro folders) extend the manifest without - changing this function's signature on the wire. + pinlock payloads via ``manifest["rtlil_file"]`` and + ``manifest["pins_lock_file"]``. Keys naming a file inside the + archive carry a ``_file`` suffix so they're distinguishable from + plain value keys (``version``, ``project``). Future additions (e.g. + macro folders) extend the manifest without changing this function's + signature on the wire. """ rtlil_arc = Path(rtlil_path).name pins_lock_arc = "pins.lock" manifest = { "version": "1", "project": project_name, - "rtlil": rtlil_arc, - "pins_lock": pins_lock_arc, + "rtlil_file": rtlil_arc, + "pins_lock_file": pins_lock_arc, } manifest_bytes = (json.dumps(manifest, indent=2) + "\n").encode("utf-8") diff --git a/tests/test_silicon_submit.py b/tests/test_silicon_submit.py index 922a4793..90bd95f8 100644 --- a/tests/test_silicon_submit.py +++ b/tests/test_silicon_submit.py @@ -196,8 +196,8 @@ def test_manifest_and_layout(self): manifest = json.loads(zf.read("manifest.json")) self.assertEqual(manifest["version"], "1") self.assertEqual(manifest["project"], "my_project") - self.assertEqual(manifest["rtlil"], "top.il") - self.assertEqual(manifest["pins_lock"], "pins.lock") + self.assertEqual(manifest["rtlil_file"], "top.il") + self.assertEqual(manifest["pins_lock_file"], "pins.lock") self.assertEqual(zf.read("top.il").decode(), "module top(); endmodule\n") self.assertEqual(zf.read("pins.lock").decode(), config) @@ -211,7 +211,7 @@ def test_uses_real_rtlil_filename(self): with zipfile.ZipFile(io.BytesIO(blob)) as zf: self.assertIn("weird_name.rtlil", zf.namelist()) manifest = json.loads(zf.read("manifest.json")) - self.assertEqual(manifest["rtlil"], "weird_name.rtlil") + self.assertEqual(manifest["rtlil_file"], "weird_name.rtlil") class TestSiliconSubmitBundlePost(unittest.TestCase): From 7e1dced1fe4ba903b5d11de5fbbe36c3bfcdb2eb Mon Sep 17 00:00:00 2001 From: Serge Rabyking Date: Wed, 29 Apr 2026 14:55:59 +0200 Subject: [PATCH 6/8] refactor(submit): rename manifest "rtlil_file" to "design_file" The manifest key now describes the role (the design's intermediate representation) rather than the format. Lets the same key carry rtlil today and another intermediate (Verilog, FIRRTL) tomorrow without another rename. The function still takes an rtlil_path parameter because that's what the caller actually has on hand from amaranth. --- chipflow/platform/silicon_step.py | 19 +++++++++++-------- tests/test_silicon_submit.py | 4 ++-- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/chipflow/platform/silicon_step.py b/chipflow/platform/silicon_step.py index 2982f28f..a8ae51b0 100644 --- a/chipflow/platform/silicon_step.py +++ b/chipflow/platform/silicon_step.py @@ -45,20 +45,23 @@ def _build_bundle_zip(rtlil_path, config: str, project_name: str) -> bytes: consumers (logs, dashboards, the backend's working directory naming) use it to identify the design without re-parsing the pinlock. - The manifest is the only contract: consumers locate the rtlil and - pinlock payloads via ``manifest["rtlil_file"]`` and + The manifest is the only contract: consumers locate the design and + pinlock payloads via ``manifest["design_file"]`` and ``manifest["pins_lock_file"]``. Keys naming a file inside the archive carry a ``_file`` suffix so they're distinguishable from - plain value keys (``version``, ``project``). Future additions (e.g. - macro folders) extend the manifest without changing this function's - signature on the wire. + plain value keys (``version``, ``project``); the value is a + zip-relative path. ``design_file`` is named in terms of role rather + than format so the same key can carry rtlil today, or another + intermediate (Verilog, FIRRTL) tomorrow, without renaming. Future + additions (e.g. macro folders) extend the manifest without + changing this function's signature on the wire. """ - rtlil_arc = Path(rtlil_path).name + design_arc = Path(rtlil_path).name pins_lock_arc = "pins.lock" manifest = { "version": "1", "project": project_name, - "rtlil_file": rtlil_arc, + "design_file": design_arc, "pins_lock_file": pins_lock_arc, } manifest_bytes = (json.dumps(manifest, indent=2) + "\n").encode("utf-8") @@ -67,7 +70,7 @@ def _build_bundle_zip(rtlil_path, config: str, project_name: str) -> bytes: with zipfile.ZipFile(buf, mode="w", compression=zipfile.ZIP_DEFLATED) as zf: zf.writestr("manifest.json", manifest_bytes) zf.writestr(pins_lock_arc, config) - zf.write(str(rtlil_path), arcname=rtlil_arc) + zf.write(str(rtlil_path), arcname=design_arc) return buf.getvalue() diff --git a/tests/test_silicon_submit.py b/tests/test_silicon_submit.py index 90bd95f8..bf10bd97 100644 --- a/tests/test_silicon_submit.py +++ b/tests/test_silicon_submit.py @@ -196,7 +196,7 @@ def test_manifest_and_layout(self): manifest = json.loads(zf.read("manifest.json")) self.assertEqual(manifest["version"], "1") self.assertEqual(manifest["project"], "my_project") - self.assertEqual(manifest["rtlil_file"], "top.il") + self.assertEqual(manifest["design_file"], "top.il") self.assertEqual(manifest["pins_lock_file"], "pins.lock") self.assertEqual(zf.read("top.il").decode(), "module top(); endmodule\n") @@ -211,7 +211,7 @@ def test_uses_real_rtlil_filename(self): with zipfile.ZipFile(io.BytesIO(blob)) as zf: self.assertIn("weird_name.rtlil", zf.namelist()) manifest = json.loads(zf.read("manifest.json")) - self.assertEqual(manifest["rtlil_file"], "weird_name.rtlil") + self.assertEqual(manifest["design_file"], "weird_name.rtlil") class TestSiliconSubmitBundlePost(unittest.TestCase): From 0cb54e431dd7bcffa486cbfe034a9d40408a88bf Mon Sep 17 00:00:00 2001 From: Serge Rabyking Date: Wed, 29 Apr 2026 15:16:25 +0200 Subject: [PATCH 7/8] feat(submit): include silicon process in bundle manifest Adds chipflow.toml [chipflow.silicon] process value (e.g. "sky130", "gf180") under a "process" key in manifest.json. The backend uses it to pick the right PDK / flow without re-parsing the pinlock. The pre-existing browser-prompt tests grew a real silicon.process.value on their config mock to satisfy the new attribute access. --- chipflow/platform/silicon_step.py | 19 ++++++++++++------- tests/test_silicon_submit.py | 13 +++++++------ 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/chipflow/platform/silicon_step.py b/chipflow/platform/silicon_step.py index a8ae51b0..e5008ecb 100644 --- a/chipflow/platform/silicon_step.py +++ b/chipflow/platform/silicon_step.py @@ -32,7 +32,7 @@ logger = logging.getLogger(__name__) -def _build_bundle_zip(rtlil_path, config: str, project_name: str) -> bytes: +def _build_bundle_zip(rtlil_path, config: str, project_name: str, process: str) -> bytes: """Pack the submission into a single zip with a manifest. Layout:: @@ -42,16 +42,18 @@ def _build_bundle_zip(rtlil_path, config: str, project_name: str) -> bytes: pins.lock # the pinlock JSON ``project_name`` is the chipflow.toml ``[chipflow] project_name`` value; - consumers (logs, dashboards, the backend's working directory naming) - use it to identify the design without re-parsing the pinlock. + ``process`` is the chipflow.toml ``[chipflow.silicon] process`` value + (e.g. "sky130", "gf180"). Consumers (logs, dashboards, the backend's + working directory naming and PDK selection) use these to identify + and route the design without re-parsing the pinlock. The manifest is the only contract: consumers locate the design and pinlock payloads via ``manifest["design_file"]`` and ``manifest["pins_lock_file"]``. Keys naming a file inside the archive carry a ``_file`` suffix so they're distinguishable from - plain value keys (``version``, ``project``); the value is a - zip-relative path. ``design_file`` is named in terms of role rather - than format so the same key can carry rtlil today, or another + plain value keys (``version``, ``project``, ``process``); the value + is a zip-relative path. ``design_file`` is named in terms of role + rather than format so the same key can carry rtlil today, or another intermediate (Verilog, FIRRTL) tomorrow, without renaming. Future additions (e.g. macro folders) extend the manifest without changing this function's signature on the wire. @@ -61,6 +63,7 @@ def _build_bundle_zip(rtlil_path, config: str, project_name: str) -> bytes: manifest = { "version": "1", "project": project_name, + "process": process, "design_file": design_arc, "pins_lock_file": pins_lock_arc, } @@ -226,7 +229,9 @@ def submit(self, rtlil_path, args): config = pinlock.model_dump_json(indent=2) bundle_bytes = _build_bundle_zip( - rtlil_path, config, self.config.chipflow.project_name) + rtlil_path, config, + self.config.chipflow.project_name, + self.config.chipflow.silicon.process.value) if args.dry_run: sp.succeed(f"✅ Design `{data['projectId']}:{data['name']}` ready for submission to ChipFlow cloud!") diff --git a/tests/test_silicon_submit.py b/tests/test_silicon_submit.py index bf10bd97..a56ed12b 100644 --- a/tests/test_silicon_submit.py +++ b/tests/test_silicon_submit.py @@ -50,7 +50,7 @@ def test_browser_prompt_yes(self, mock_subprocess, mock_isatty, mock_input, mock # Create a mock SiliconStep instance with mock.patch('chipflow.platform.silicon_step.SiliconPlatform'): config = mock.MagicMock() - config.chipflow.silicon = True + config.chipflow.silicon.process.value = 'sky130' config.chipflow.project_name = 'test_project' step = SiliconStep(config) step._build_url = "https://build.chipflow.com/build/test123" @@ -99,7 +99,7 @@ def test_browser_prompt_no(self, mock_subprocess, mock_isatty, mock_input, mock_ # Create a mock SiliconStep instance with mock.patch('chipflow.platform.silicon_step.SiliconPlatform'): config = mock.MagicMock() - config.chipflow.silicon = True + config.chipflow.silicon.process.value = 'sky130' config.chipflow.project_name = 'test_project' step = SiliconStep(config) step._build_url = "https://build.chipflow.com/build/test123" @@ -146,7 +146,7 @@ def test_browser_prompt_not_tty(self, mock_subprocess, mock_isatty, mock_input, # Create a mock SiliconStep instance with mock.patch('chipflow.platform.silicon_step.SiliconPlatform'): config = mock.MagicMock() - config.chipflow.silicon = True + config.chipflow.silicon.process.value = 'sky130' config.chipflow.project_name = 'test_project' step = SiliconStep(config) step._build_url = "https://build.chipflow.com/build/test123" @@ -187,7 +187,7 @@ def test_manifest_and_layout(self): rtlil_path.write_text("module top(); endmodule\n") config = '{"pins": []}' - blob = _build_bundle_zip(rtlil_path, config, "my_project") + blob = _build_bundle_zip(rtlil_path, config, "my_project", "sky130") with zipfile.ZipFile(io.BytesIO(blob)) as zf: names = set(zf.namelist()) @@ -196,6 +196,7 @@ def test_manifest_and_layout(self): manifest = json.loads(zf.read("manifest.json")) self.assertEqual(manifest["version"], "1") self.assertEqual(manifest["project"], "my_project") + self.assertEqual(manifest["process"], "sky130") self.assertEqual(manifest["design_file"], "top.il") self.assertEqual(manifest["pins_lock_file"], "pins.lock") @@ -207,7 +208,7 @@ def test_uses_real_rtlil_filename(self): with tempfile.TemporaryDirectory() as td: rtlil_path = Path(td) / "weird_name.rtlil" rtlil_path.write_text("x") - blob = _build_bundle_zip(rtlil_path, "{}", "p") + blob = _build_bundle_zip(rtlil_path, "{}", "p", "sky130") with zipfile.ZipFile(io.BytesIO(blob)) as zf: self.assertIn("weird_name.rtlil", zf.namelist()) manifest = json.loads(zf.read("manifest.json")) @@ -227,7 +228,7 @@ def test_submit_sends_single_bundle_part(self, mock_subprocess, mock_load_pinloc with mock.patch('chipflow.platform.silicon_step.SiliconPlatform'): config = mock.MagicMock() - config.chipflow.silicon = True + config.chipflow.silicon.process.value = 'sky130' config.chipflow.project_name = 'test_project' step = SiliconStep(config) step.platform._ports = {} From f508d47d571cbdf159aff360dbf0f0597f0d1919 Mon Sep 17 00:00:00 2001 From: Serge Rabyking Date: Wed, 29 Apr 2026 15:45:37 +0200 Subject: [PATCH 8/8] feat(submit): include silicon package in bundle manifest Adds chipflow.toml [chipflow.silicon] package value (e.g. "cf20") under a "package" key in manifest.json. Together with "process", lets the backend pick the right PDK + package combination without re-parsing the pinlock. --- chipflow/platform/silicon_step.py | 30 +++++++++++++++++------------- tests/test_silicon_submit.py | 9 +++++++-- 2 files changed, 24 insertions(+), 15 deletions(-) diff --git a/chipflow/platform/silicon_step.py b/chipflow/platform/silicon_step.py index e5008ecb..6bd15e40 100644 --- a/chipflow/platform/silicon_step.py +++ b/chipflow/platform/silicon_step.py @@ -32,7 +32,9 @@ logger = logging.getLogger(__name__) -def _build_bundle_zip(rtlil_path, config: str, project_name: str, process: str) -> bytes: +def _build_bundle_zip( + rtlil_path, config: str, project_name: str, process: str, package: str +) -> bytes: """Pack the submission into a single zip with a manifest. Layout:: @@ -41,22 +43,22 @@ def _build_bundle_zip(rtlil_path, config: str, project_name: str, process: str) # e.g. "top.il", taken from rtlil_path pins.lock # the pinlock JSON - ``project_name`` is the chipflow.toml ``[chipflow] project_name`` value; - ``process`` is the chipflow.toml ``[chipflow.silicon] process`` value - (e.g. "sky130", "gf180"). Consumers (logs, dashboards, the backend's - working directory naming and PDK selection) use these to identify - and route the design without re-parsing the pinlock. + ``project_name`` / ``process`` / ``package`` come from chipflow.toml + (``[chipflow] project_name``, ``[chipflow.silicon] process``, + ``[chipflow.silicon] package``). Consumers (logs, dashboards, the + backend's working directory naming and PDK / package selection) use + these to identify and route the design without re-parsing the pinlock. The manifest is the only contract: consumers locate the design and pinlock payloads via ``manifest["design_file"]`` and ``manifest["pins_lock_file"]``. Keys naming a file inside the archive carry a ``_file`` suffix so they're distinguishable from - plain value keys (``version``, ``project``, ``process``); the value - is a zip-relative path. ``design_file`` is named in terms of role - rather than format so the same key can carry rtlil today, or another - intermediate (Verilog, FIRRTL) tomorrow, without renaming. Future - additions (e.g. macro folders) extend the manifest without - changing this function's signature on the wire. + plain value keys (``version``, ``project``, ``process``, + ``package``); the value is a zip-relative path. ``design_file`` is + named in terms of role rather than format so the same key can carry + rtlil today, or another intermediate (Verilog, FIRRTL) tomorrow, + without renaming. Future additions (e.g. macro folders) extend the + manifest without changing this function's signature on the wire. """ design_arc = Path(rtlil_path).name pins_lock_arc = "pins.lock" @@ -64,6 +66,7 @@ def _build_bundle_zip(rtlil_path, config: str, project_name: str, process: str) "version": "1", "project": project_name, "process": process, + "package": package, "design_file": design_arc, "pins_lock_file": pins_lock_arc, } @@ -231,7 +234,8 @@ def submit(self, rtlil_path, args): bundle_bytes = _build_bundle_zip( rtlil_path, config, self.config.chipflow.project_name, - self.config.chipflow.silicon.process.value) + self.config.chipflow.silicon.process.value, + self.config.chipflow.silicon.package) if args.dry_run: sp.succeed(f"✅ Design `{data['projectId']}:{data['name']}` ready for submission to ChipFlow cloud!") diff --git a/tests/test_silicon_submit.py b/tests/test_silicon_submit.py index a56ed12b..fabf0853 100644 --- a/tests/test_silicon_submit.py +++ b/tests/test_silicon_submit.py @@ -51,6 +51,7 @@ def test_browser_prompt_yes(self, mock_subprocess, mock_isatty, mock_input, mock with mock.patch('chipflow.platform.silicon_step.SiliconPlatform'): config = mock.MagicMock() config.chipflow.silicon.process.value = 'sky130' + config.chipflow.silicon.package = 'cf20' config.chipflow.project_name = 'test_project' step = SiliconStep(config) step._build_url = "https://build.chipflow.com/build/test123" @@ -100,6 +101,7 @@ def test_browser_prompt_no(self, mock_subprocess, mock_isatty, mock_input, mock_ with mock.patch('chipflow.platform.silicon_step.SiliconPlatform'): config = mock.MagicMock() config.chipflow.silicon.process.value = 'sky130' + config.chipflow.silicon.package = 'cf20' config.chipflow.project_name = 'test_project' step = SiliconStep(config) step._build_url = "https://build.chipflow.com/build/test123" @@ -147,6 +149,7 @@ def test_browser_prompt_not_tty(self, mock_subprocess, mock_isatty, mock_input, with mock.patch('chipflow.platform.silicon_step.SiliconPlatform'): config = mock.MagicMock() config.chipflow.silicon.process.value = 'sky130' + config.chipflow.silicon.package = 'cf20' config.chipflow.project_name = 'test_project' step = SiliconStep(config) step._build_url = "https://build.chipflow.com/build/test123" @@ -187,7 +190,7 @@ def test_manifest_and_layout(self): rtlil_path.write_text("module top(); endmodule\n") config = '{"pins": []}' - blob = _build_bundle_zip(rtlil_path, config, "my_project", "sky130") + blob = _build_bundle_zip(rtlil_path, config, "my_project", "sky130", "cf20") with zipfile.ZipFile(io.BytesIO(blob)) as zf: names = set(zf.namelist()) @@ -197,6 +200,7 @@ def test_manifest_and_layout(self): self.assertEqual(manifest["version"], "1") self.assertEqual(manifest["project"], "my_project") self.assertEqual(manifest["process"], "sky130") + self.assertEqual(manifest["package"], "cf20") self.assertEqual(manifest["design_file"], "top.il") self.assertEqual(manifest["pins_lock_file"], "pins.lock") @@ -208,7 +212,7 @@ def test_uses_real_rtlil_filename(self): with tempfile.TemporaryDirectory() as td: rtlil_path = Path(td) / "weird_name.rtlil" rtlil_path.write_text("x") - blob = _build_bundle_zip(rtlil_path, "{}", "p", "sky130") + blob = _build_bundle_zip(rtlil_path, "{}", "p", "sky130", "cf20") with zipfile.ZipFile(io.BytesIO(blob)) as zf: self.assertIn("weird_name.rtlil", zf.namelist()) manifest = json.loads(zf.read("manifest.json")) @@ -229,6 +233,7 @@ def test_submit_sends_single_bundle_part(self, mock_subprocess, mock_load_pinloc with mock.patch('chipflow.platform.silicon_step.SiliconPlatform'): config = mock.MagicMock() config.chipflow.silicon.process.value = 'sky130' + config.chipflow.silicon.package = 'cf20' config.chipflow.project_name = 'test_project' step = SiliconStep(config) step.platform._ports = {}