chipfoundry-cli 2.6.0__tar.gz → 2.7.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- chipfoundry_cli-2.6.0/README.md → chipfoundry_cli-2.7.0/PKG-INFO +54 -5
- chipfoundry_cli-2.6.0/PKG-INFO → chipfoundry_cli-2.7.0/README.md +24 -32
- {chipfoundry_cli-2.6.0 → chipfoundry_cli-2.7.0}/chipfoundry_cli/__init__.py +1 -1
- {chipfoundry_cli-2.6.0 → chipfoundry_cli-2.7.0}/chipfoundry_cli/librelane_run.py +140 -0
- {chipfoundry_cli-2.6.0 → chipfoundry_cli-2.7.0}/chipfoundry_cli/main.py +379 -70
- {chipfoundry_cli-2.6.0 → chipfoundry_cli-2.7.0}/chipfoundry_cli/remote_precheck_git.py +6 -3
- {chipfoundry_cli-2.6.0 → chipfoundry_cli-2.7.0}/chipfoundry_cli/utils.py +55 -4
- {chipfoundry_cli-2.6.0 → chipfoundry_cli-2.7.0}/pyproject.toml +1 -1
- {chipfoundry_cli-2.6.0 → chipfoundry_cli-2.7.0}/LICENSE +0 -0
- {chipfoundry_cli-2.6.0 → chipfoundry_cli-2.7.0}/chipfoundry_cli/check_refs.py +0 -0
- {chipfoundry_cli-2.6.0 → chipfoundry_cli-2.7.0}/chipfoundry_cli/preview.py +0 -0
- {chipfoundry_cli-2.6.0 → chipfoundry_cli-2.7.0}/chipfoundry_cli/version_check.py +0 -0
|
@@ -1,3 +1,32 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: chipfoundry-cli
|
|
3
|
+
Version: 2.7.0
|
|
4
|
+
Summary: CLI tool to automate ChipFoundry project submission to SFTP server
|
|
5
|
+
License: Apache-2.0
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Author: ChipFoundry
|
|
8
|
+
Author-email: marwan.abbas@chipfoundry.io
|
|
9
|
+
Requires-Python: >=3.8.1,<4.0
|
|
10
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
19
|
+
Requires-Dist: cf-ipm
|
|
20
|
+
Requires-Dist: click (>=8.0.0,<9)
|
|
21
|
+
Requires-Dist: httpx (>=0.24.0,<1.0)
|
|
22
|
+
Requires-Dist: paramiko (>=3.0.0,<4)
|
|
23
|
+
Requires-Dist: rich (>=13,<14)
|
|
24
|
+
Requires-Dist: textual (>=0.40.0,<1)
|
|
25
|
+
Requires-Dist: toml (>=0.10,<1.0)
|
|
26
|
+
Project-URL: Homepage, https://chipfoundry.io
|
|
27
|
+
Project-URL: Repository, https://github.com/chipfoundry/cf-cli
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
|
|
1
30
|
# ChipFoundry CLI (`cf-cli`)
|
|
2
31
|
|
|
3
32
|
[](https://badge.fury.io/py/chipfoundry-cli)
|
|
@@ -112,6 +141,24 @@ If you already have a project directory with GDS files:
|
|
|
112
141
|
|
|
113
142
|
---
|
|
114
143
|
|
|
144
|
+
## Double-wide openframe projects
|
|
145
|
+
|
|
146
|
+
Double-wide projects (`type: double_wide`, detected from
|
|
147
|
+
`gds/double_wide_openframe_project_wrapper.gds`) use the same commands as
|
|
148
|
+
openframe projects, with these differences:
|
|
149
|
+
|
|
150
|
+
- `cf setup` clones the double-wide harness
|
|
151
|
+
([chipfoundry/double_wide_openframe](https://github.com/chipfoundry/double_wide_openframe))
|
|
152
|
+
into `caravel/` instead of caravel-lite, and skips the management core wrapper.
|
|
153
|
+
The harness ref comes from `harness_refs.double_wide` in `versions.json` (default `main`).
|
|
154
|
+
- `cf precheck` passes `caravel/` to cf-precheck as the golden root, since the
|
|
155
|
+
precheck image only ships the standard caravel.
|
|
156
|
+
- `cf verify` runs caravel-cocotb in OpenFrame mode. The harness supplies the
|
|
157
|
+
63-pad testbench (`verilog/rtl/toplevel_cocotb.v`) and include lists.
|
|
158
|
+
- `cf gpio-config` is not used: the user wrapper drives every pad control signal.
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
115
162
|
## Project Structure Requirements
|
|
116
163
|
|
|
117
164
|
Your project directory **must** contain:
|
|
@@ -120,6 +167,7 @@ Your project directory **must** contain:
|
|
|
120
167
|
- `user_project_wrapper.gds` (for digital projects)
|
|
121
168
|
- `user_analog_project_wrapper.gds` (for analog projects)
|
|
122
169
|
- `openframe_project_wrapper.gds` (for openframe projects)
|
|
170
|
+
- `double_wide_openframe_project_wrapper.gds` (for double-wide openframe projects)
|
|
123
171
|
- **Note**: Both compressed (`.gz`) and uncompressed (`.gds`) files are supported
|
|
124
172
|
- `verilog/rtl/user_defines.v` (required for digital/analog)
|
|
125
173
|
- `.cf/project.json` (optional; will be created/updated automatically)
|
|
@@ -617,7 +665,7 @@ Preconditions:
|
|
|
617
665
|
- Project has a GitHub repo URL (set via `cf init`, shown in the portal).
|
|
618
666
|
- The ChipFoundry GitHub App is installed on that repo (prompted in the portal).
|
|
619
667
|
- Your local `HEAD` has been pushed to `origin` on some branch (`git push`).
|
|
620
|
-
- Push-critical files at `HEAD` are clean: wrapper GDS, `verilog/rtl/user_defines.v` (
|
|
668
|
+
- Push-critical files at `HEAD` are clean: wrapper GDS, `verilog/rtl/user_defines.v` (digital and analog projects only), and `.cf/project.json` (when tracked).
|
|
621
669
|
|
|
622
670
|
What happens:
|
|
623
671
|
1. `cf push --remote` resolves your local HEAD SHA and checks it is reachable from a remote ref.
|
|
@@ -641,8 +689,8 @@ both port 22 and GitHub.
|
|
|
641
689
|
Preconditions:
|
|
642
690
|
- Project is linked to the platform (`cf init` or `cf link`) and you are logged in.
|
|
643
691
|
- Wrapper GDS exists locally under `gds/` (one of `user_project_wrapper.gds[.gz]`,
|
|
644
|
-
`user_analog_project_wrapper.gds[.gz]`, `openframe_project_wrapper.gds[.gz]`).
|
|
645
|
-
- For
|
|
692
|
+
`user_analog_project_wrapper.gds[.gz]`, `openframe_project_wrapper.gds[.gz]`, `double_wide_openframe_project_wrapper.gds[.gz]`).
|
|
693
|
+
- For digital and analog projects, `verilog/rtl/user_defines.v` is also uploaded when present.
|
|
646
694
|
- Outbound HTTPS to `*.s3.us-east-2.amazonaws.com` is allowed.
|
|
647
695
|
|
|
648
696
|
What happens:
|
|
@@ -863,7 +911,7 @@ The CLI tracks your project submission state through the `submission_state` fiel
|
|
|
863
911
|
|
|
864
912
|
1. **File Collection:**
|
|
865
913
|
- Checks for required GDS and Verilog files
|
|
866
|
-
- Auto-detects project type (digital, analog, openframe) based on GDS file name
|
|
914
|
+
- Auto-detects project type (digital, analog, openframe, double_wide) based on GDS file name
|
|
867
915
|
- **Supports both compressed and uncompressed GDS files**
|
|
868
916
|
|
|
869
917
|
2. **Configuration:**
|
|
@@ -932,7 +980,7 @@ cf config
|
|
|
932
980
|
# Initialize project (in your project directory)
|
|
933
981
|
cf init
|
|
934
982
|
# Project name (detected: my_awesome_project):
|
|
935
|
-
# Project type (digital/analog/openframe) (detected: digital):
|
|
983
|
+
# Project type (digital/analog/openframe/double_wide) (detected: digital):
|
|
936
984
|
|
|
937
985
|
# Upload your project
|
|
938
986
|
cf push
|
|
@@ -1050,3 +1098,4 @@ gds/user_project_wrapper.gds.gz # ← Remove this one
|
|
|
1050
1098
|
|
|
1051
1099
|
- For help, contact info@chipfoundry.io or visit [chipfoundry.io](https://chipfoundry.io)
|
|
1052
1100
|
- For bug reports or feature requests, open an issue on [GitHub](https://github.com/chipfoundry/cf-cli)
|
|
1101
|
+
|
|
@@ -1,29 +1,3 @@
|
|
|
1
|
-
Metadata-Version: 2.1
|
|
2
|
-
Name: chipfoundry-cli
|
|
3
|
-
Version: 2.6.0
|
|
4
|
-
Summary: CLI tool to automate ChipFoundry project submission to SFTP server
|
|
5
|
-
Home-page: https://chipfoundry.io
|
|
6
|
-
License: Apache-2.0
|
|
7
|
-
Author: ChipFoundry
|
|
8
|
-
Author-email: marwan.abbas@chipfoundry.io
|
|
9
|
-
Requires-Python: >=3.8.1,<4.0
|
|
10
|
-
Classifier: License :: OSI Approved :: Apache Software License
|
|
11
|
-
Classifier: Programming Language :: Python :: 3
|
|
12
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
13
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
-
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
-
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
-
Requires-Dist: cf-ipm
|
|
18
|
-
Requires-Dist: click (>=8.0.0,<9)
|
|
19
|
-
Requires-Dist: httpx (>=0.24.0,<1.0)
|
|
20
|
-
Requires-Dist: paramiko (>=3.0.0,<4)
|
|
21
|
-
Requires-Dist: rich (>=13,<14)
|
|
22
|
-
Requires-Dist: textual (>=0.40.0,<1)
|
|
23
|
-
Requires-Dist: toml (>=0.10,<1.0)
|
|
24
|
-
Project-URL: Repository, https://github.com/chipfoundry/cf-cli
|
|
25
|
-
Description-Content-Type: text/markdown
|
|
26
|
-
|
|
27
1
|
# ChipFoundry CLI (`cf-cli`)
|
|
28
2
|
|
|
29
3
|
[](https://badge.fury.io/py/chipfoundry-cli)
|
|
@@ -138,6 +112,24 @@ If you already have a project directory with GDS files:
|
|
|
138
112
|
|
|
139
113
|
---
|
|
140
114
|
|
|
115
|
+
## Double-wide openframe projects
|
|
116
|
+
|
|
117
|
+
Double-wide projects (`type: double_wide`, detected from
|
|
118
|
+
`gds/double_wide_openframe_project_wrapper.gds`) use the same commands as
|
|
119
|
+
openframe projects, with these differences:
|
|
120
|
+
|
|
121
|
+
- `cf setup` clones the double-wide harness
|
|
122
|
+
([chipfoundry/double_wide_openframe](https://github.com/chipfoundry/double_wide_openframe))
|
|
123
|
+
into `caravel/` instead of caravel-lite, and skips the management core wrapper.
|
|
124
|
+
The harness ref comes from `harness_refs.double_wide` in `versions.json` (default `main`).
|
|
125
|
+
- `cf precheck` passes `caravel/` to cf-precheck as the golden root, since the
|
|
126
|
+
precheck image only ships the standard caravel.
|
|
127
|
+
- `cf verify` runs caravel-cocotb in OpenFrame mode. The harness supplies the
|
|
128
|
+
63-pad testbench (`verilog/rtl/toplevel_cocotb.v`) and include lists.
|
|
129
|
+
- `cf gpio-config` is not used: the user wrapper drives every pad control signal.
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
141
133
|
## Project Structure Requirements
|
|
142
134
|
|
|
143
135
|
Your project directory **must** contain:
|
|
@@ -146,6 +138,7 @@ Your project directory **must** contain:
|
|
|
146
138
|
- `user_project_wrapper.gds` (for digital projects)
|
|
147
139
|
- `user_analog_project_wrapper.gds` (for analog projects)
|
|
148
140
|
- `openframe_project_wrapper.gds` (for openframe projects)
|
|
141
|
+
- `double_wide_openframe_project_wrapper.gds` (for double-wide openframe projects)
|
|
149
142
|
- **Note**: Both compressed (`.gz`) and uncompressed (`.gds`) files are supported
|
|
150
143
|
- `verilog/rtl/user_defines.v` (required for digital/analog)
|
|
151
144
|
- `.cf/project.json` (optional; will be created/updated automatically)
|
|
@@ -643,7 +636,7 @@ Preconditions:
|
|
|
643
636
|
- Project has a GitHub repo URL (set via `cf init`, shown in the portal).
|
|
644
637
|
- The ChipFoundry GitHub App is installed on that repo (prompted in the portal).
|
|
645
638
|
- Your local `HEAD` has been pushed to `origin` on some branch (`git push`).
|
|
646
|
-
- Push-critical files at `HEAD` are clean: wrapper GDS, `verilog/rtl/user_defines.v` (
|
|
639
|
+
- Push-critical files at `HEAD` are clean: wrapper GDS, `verilog/rtl/user_defines.v` (digital and analog projects only), and `.cf/project.json` (when tracked).
|
|
647
640
|
|
|
648
641
|
What happens:
|
|
649
642
|
1. `cf push --remote` resolves your local HEAD SHA and checks it is reachable from a remote ref.
|
|
@@ -667,8 +660,8 @@ both port 22 and GitHub.
|
|
|
667
660
|
Preconditions:
|
|
668
661
|
- Project is linked to the platform (`cf init` or `cf link`) and you are logged in.
|
|
669
662
|
- Wrapper GDS exists locally under `gds/` (one of `user_project_wrapper.gds[.gz]`,
|
|
670
|
-
`user_analog_project_wrapper.gds[.gz]`, `openframe_project_wrapper.gds[.gz]`).
|
|
671
|
-
- For
|
|
663
|
+
`user_analog_project_wrapper.gds[.gz]`, `openframe_project_wrapper.gds[.gz]`, `double_wide_openframe_project_wrapper.gds[.gz]`).
|
|
664
|
+
- For digital and analog projects, `verilog/rtl/user_defines.v` is also uploaded when present.
|
|
672
665
|
- Outbound HTTPS to `*.s3.us-east-2.amazonaws.com` is allowed.
|
|
673
666
|
|
|
674
667
|
What happens:
|
|
@@ -889,7 +882,7 @@ The CLI tracks your project submission state through the `submission_state` fiel
|
|
|
889
882
|
|
|
890
883
|
1. **File Collection:**
|
|
891
884
|
- Checks for required GDS and Verilog files
|
|
892
|
-
- Auto-detects project type (digital, analog, openframe) based on GDS file name
|
|
885
|
+
- Auto-detects project type (digital, analog, openframe, double_wide) based on GDS file name
|
|
893
886
|
- **Supports both compressed and uncompressed GDS files**
|
|
894
887
|
|
|
895
888
|
2. **Configuration:**
|
|
@@ -958,7 +951,7 @@ cf config
|
|
|
958
951
|
# Initialize project (in your project directory)
|
|
959
952
|
cf init
|
|
960
953
|
# Project name (detected: my_awesome_project):
|
|
961
|
-
# Project type (digital/analog/openframe) (detected: digital):
|
|
954
|
+
# Project type (digital/analog/openframe/double_wide) (detected: digital):
|
|
962
955
|
|
|
963
956
|
# Upload your project
|
|
964
957
|
cf push
|
|
@@ -1076,4 +1069,3 @@ gds/user_project_wrapper.gds.gz # ← Remove this one
|
|
|
1076
1069
|
|
|
1077
1070
|
- For help, contact info@chipfoundry.io or visit [chipfoundry.io](https://chipfoundry.io)
|
|
1078
1071
|
- For bug reports or feature requests, open an issue on [GitHub](https://github.com/chipfoundry/cf-cli)
|
|
1079
|
-
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""ChipFoundry CLI package: Automate project submission to SFTP."""
|
|
2
|
-
__version__ = "2.
|
|
2
|
+
__version__ = "2.7.0"
|
|
@@ -228,6 +228,146 @@ def new_run_tag() -> str:
|
|
|
228
228
|
return datetime.now().strftime("%y_%m_%d_%H_%M")
|
|
229
229
|
|
|
230
230
|
|
|
231
|
+
def double_wide_flake_dir(project_root: Path) -> Optional[Path]:
|
|
232
|
+
"""The double_wide project's own flake.nix dir, pinning LibreLane 3.
|
|
233
|
+
|
|
234
|
+
double_wide ships its own flake (not cf-cli's `chipfoundry/openlane-2`
|
|
235
|
+
flake used by every other project type) because the flat wrapper needs
|
|
236
|
+
LibreLane 3's native PDN-to-pad routing. Returns None if the project
|
|
237
|
+
doesn't have one (e.g. it predates this, or a user deleted it).
|
|
238
|
+
"""
|
|
239
|
+
if (project_root / "flake.nix").is_file():
|
|
240
|
+
return project_root
|
|
241
|
+
return None
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def double_wide_nix_available(flake_dir: Path, timeout: float = 20) -> bool:
|
|
245
|
+
"""Check that `nix develop` can resolve the project's pinned LibreLane 3 flake."""
|
|
246
|
+
if shutil.which("nix") is None:
|
|
247
|
+
return False
|
|
248
|
+
try:
|
|
249
|
+
result = subprocess.run(
|
|
250
|
+
["nix", "flake", "metadata", str(flake_dir), "--json"],
|
|
251
|
+
capture_output=True,
|
|
252
|
+
timeout=timeout,
|
|
253
|
+
)
|
|
254
|
+
return result.returncode == 0
|
|
255
|
+
except (OSError, subprocess.TimeoutExpired):
|
|
256
|
+
return False
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def build_double_wide_librelane_command(
|
|
260
|
+
*,
|
|
261
|
+
project_root: Path,
|
|
262
|
+
flake_dir: Path,
|
|
263
|
+
config_file: Path,
|
|
264
|
+
pdk_root: Path,
|
|
265
|
+
pdk: str,
|
|
266
|
+
tag: str,
|
|
267
|
+
from_step: Optional[str] = None,
|
|
268
|
+
open_in_openroad: bool = False,
|
|
269
|
+
open_in_klayout: bool = False,
|
|
270
|
+
) -> Tuple[List[str], Dict[str, str]]:
|
|
271
|
+
"""Build the `nix develop` command that runs LibreLane 3 on a flat double_wide wrapper.
|
|
272
|
+
|
|
273
|
+
No `--manual-pdk`/`--pdk-root`/`--pdk` flags: LibreLane 3's default
|
|
274
|
+
(Ciel-managed) PDK resolution auto-fetches the pinned PDK commit into
|
|
275
|
+
`PDK_ROOT` on its own, the same project-local `dependencies/pdks` root
|
|
276
|
+
every other project type's PDK already lives under (Ciel keys PDK
|
|
277
|
+
versions by commit, so the two coexist without conflict).
|
|
278
|
+
"""
|
|
279
|
+
env = os.environ.copy()
|
|
280
|
+
env.update(
|
|
281
|
+
{
|
|
282
|
+
"PROJECT_ROOT": str(project_root),
|
|
283
|
+
"PDK_ROOT": str(pdk_root),
|
|
284
|
+
"PDK": pdk,
|
|
285
|
+
"LIBRELANE_RUN_TAG": tag,
|
|
286
|
+
}
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
librelane_args = []
|
|
290
|
+
if open_in_openroad:
|
|
291
|
+
librelane_args.extend(["--flow", "OpenInOpenROAD"])
|
|
292
|
+
elif open_in_klayout:
|
|
293
|
+
librelane_args.extend(["--flow", "OpenInKLayout"])
|
|
294
|
+
librelane_args.extend(["--ef-save-views-to", str(project_root), "--run-tag", tag])
|
|
295
|
+
if not from_step and not (open_in_openroad or open_in_klayout):
|
|
296
|
+
librelane_args.append("--overwrite")
|
|
297
|
+
if from_step:
|
|
298
|
+
librelane_args.extend(["--from", from_step])
|
|
299
|
+
librelane_args.append(str(config_file))
|
|
300
|
+
|
|
301
|
+
cmd = ["nix", "develop", str(flake_dir), "--command", "librelane", *librelane_args]
|
|
302
|
+
return cmd, env
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def get_double_wide_valid_from_steps(
|
|
306
|
+
*, flake_dir: Path, config_file: Path, working_dir: Path
|
|
307
|
+
) -> Tuple[Optional[List[str]], Optional[str]]:
|
|
308
|
+
"""Resolve valid `--from` step names for a double_wide config via LibreLane 3."""
|
|
309
|
+
script = (
|
|
310
|
+
"import json, sys\n"
|
|
311
|
+
"from librelane.config import Config\n"
|
|
312
|
+
"from librelane.flows import Flow, SequentialFlow\n"
|
|
313
|
+
"cfg = sys.argv[1]\n"
|
|
314
|
+
"target = Flow.factory.get('Classic')\n"
|
|
315
|
+
"meta = Config.get_meta(cfg)\n"
|
|
316
|
+
"if meta:\n"
|
|
317
|
+
" if isinstance(meta.flow, str):\n"
|
|
318
|
+
" found = Flow.factory.get(meta.flow)\n"
|
|
319
|
+
" if found is None:\n"
|
|
320
|
+
" raise RuntimeError(f\"Unknown flow '{meta.flow}' in config metadata\")\n"
|
|
321
|
+
" target = found\n"
|
|
322
|
+
" elif isinstance(meta.flow, list):\n"
|
|
323
|
+
" target = SequentialFlow.make(meta.flow)\n"
|
|
324
|
+
" if meta.substituting_steps is not None:\n"
|
|
325
|
+
" if meta.flow is None:\n"
|
|
326
|
+
" raise RuntimeError('substituting_steps is set but flow is not defined')\n"
|
|
327
|
+
" if not issubclass(target, SequentialFlow):\n"
|
|
328
|
+
" raise RuntimeError('substituting_steps requires a sequential flow')\n"
|
|
329
|
+
" target = target.Substitute(meta.substituting_steps)\n"
|
|
330
|
+
"steps = []\n"
|
|
331
|
+
"seen = set()\n"
|
|
332
|
+
"for step in getattr(target, 'Steps', []) or []:\n"
|
|
333
|
+
" step_id = getattr(step, 'id', None)\n"
|
|
334
|
+
" if not step_id or step_id in seen:\n"
|
|
335
|
+
" continue\n"
|
|
336
|
+
" seen.add(step_id)\n"
|
|
337
|
+
" steps.append(step_id)\n"
|
|
338
|
+
"print('__CF_STEPS__' + json.dumps({'steps': steps}))\n"
|
|
339
|
+
)
|
|
340
|
+
try:
|
|
341
|
+
result = subprocess.run(
|
|
342
|
+
[
|
|
343
|
+
"nix",
|
|
344
|
+
"develop",
|
|
345
|
+
str(flake_dir),
|
|
346
|
+
"--command",
|
|
347
|
+
"python3",
|
|
348
|
+
"-c",
|
|
349
|
+
script,
|
|
350
|
+
str(config_file),
|
|
351
|
+
],
|
|
352
|
+
cwd=str(working_dir),
|
|
353
|
+
capture_output=True,
|
|
354
|
+
text=True,
|
|
355
|
+
timeout=120,
|
|
356
|
+
)
|
|
357
|
+
except (OSError, subprocess.TimeoutExpired) as exc:
|
|
358
|
+
return None, str(exc)
|
|
359
|
+
if result.returncode != 0:
|
|
360
|
+
return None, (result.stderr or result.stdout or "unknown error").strip()
|
|
361
|
+
marker = "__CF_STEPS__"
|
|
362
|
+
for line in result.stdout.splitlines():
|
|
363
|
+
if line.startswith(marker):
|
|
364
|
+
try:
|
|
365
|
+
return json.loads(line[len(marker):])["steps"], None
|
|
366
|
+
except Exception as exc:
|
|
367
|
+
return None, str(exc)
|
|
368
|
+
return None, "LibreLane did not report a step list"
|
|
369
|
+
|
|
370
|
+
|
|
231
371
|
def find_synth_stat_json(flow_root: Path, macro: str, run_tag: str) -> Optional[Path]:
|
|
232
372
|
"""Locate Yosys stat.json written by a synth-only or full LibreLane run."""
|
|
233
373
|
run_dir = flow_root / macro / "runs" / run_tag
|
|
@@ -18,7 +18,10 @@ from chipfoundry_cli.utils import (
|
|
|
18
18
|
get_gpio_config_from_project_json, save_gpio_config_to_project_json,
|
|
19
19
|
GPIO_MODES, GPIO_MODE_DESCRIPTIONS, GPIO_HEX_TO_MODE,
|
|
20
20
|
detect_github_repo_url, get_head_commit_sha,
|
|
21
|
+
PROJECT_TYPES, HARNESS_REPOS, is_padframe_only, harness_ref,
|
|
22
|
+
read_project_type, DOUBLE_WIDE_LIBRELANE_REV,
|
|
21
23
|
)
|
|
24
|
+
from chipfoundry_cli import librelane_run
|
|
22
25
|
import os
|
|
23
26
|
from pathlib import Path
|
|
24
27
|
from rich.console import Console
|
|
@@ -33,6 +36,7 @@ import sys
|
|
|
33
36
|
import shutil
|
|
34
37
|
import signal
|
|
35
38
|
import difflib
|
|
39
|
+
import shlex
|
|
36
40
|
|
|
37
41
|
# Textual imports for GPIO grid UI
|
|
38
42
|
from textual.app import App, ComposeResult
|
|
@@ -568,11 +572,16 @@ def init(project_root, shuttle, description):
|
|
|
568
572
|
|
|
569
573
|
name = _prompt_with_default("Project name", current_name, default_name) or default_name
|
|
570
574
|
project_type = _prompt_with_default(
|
|
571
|
-
"Project type (
|
|
575
|
+
f"Project type ({'/'.join(PROJECT_TYPES)})", current_type, detected_type
|
|
572
576
|
)
|
|
573
577
|
if not project_type:
|
|
574
578
|
console.print("[red]Project type is required.[/red]")
|
|
575
579
|
raise click.Abort()
|
|
580
|
+
if project_type not in PROJECT_TYPES:
|
|
581
|
+
console.print(
|
|
582
|
+
f"[red]Unknown project type '{project_type}'. Expected one of: {', '.join(PROJECT_TYPES)}.[/red]"
|
|
583
|
+
)
|
|
584
|
+
raise click.Abort()
|
|
576
585
|
|
|
577
586
|
if description is not None:
|
|
578
587
|
description_val: Optional[str] = description or None
|
|
@@ -780,10 +789,11 @@ def gpio_config(project_root, view):
|
|
|
780
789
|
project_data = json.load(f)
|
|
781
790
|
project_type = project_data.get('project', {}).get('type', 'digital')
|
|
782
791
|
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
console.print(
|
|
786
|
-
|
|
792
|
+
if is_padframe_only(project_type):
|
|
793
|
+
console.print(f"[red]GPIO configuration is not available for {project_type} projects.[/red]")
|
|
794
|
+
console.print(
|
|
795
|
+
f"[yellow]{project_type} projects do not use user_defines.v; the user wrapper drives the pad controls.[/yellow]"
|
|
796
|
+
)
|
|
787
797
|
raise click.Abort()
|
|
788
798
|
|
|
789
799
|
user_defines_path = project_root / 'verilog' / 'rtl' / 'user_defines.v'
|
|
@@ -1772,8 +1782,8 @@ def _collect_push_candidates(project_root: Path) -> List[Tuple[str, Path, int, s
|
|
|
1772
1782
|
"""Return [(rel_path, abs_path, size, kind)] for files the platform
|
|
1773
1783
|
accepts via --https.
|
|
1774
1784
|
|
|
1775
|
-
Picks exactly one wrapper GDS (analog/digital/openframe) and,
|
|
1776
|
-
|
|
1785
|
+
Picks exactly one wrapper GDS (analog/digital/openframe/double_wide) and,
|
|
1786
|
+
for caravel-based projects, ``verilog/rtl/user_defines.v`` if it exists.
|
|
1777
1787
|
Raises FileNotFoundError if no wrapper is present or
|
|
1778
1788
|
ValueError if multiple are.
|
|
1779
1789
|
"""
|
|
@@ -1790,7 +1800,8 @@ def _collect_push_candidates(project_root: Path) -> List[Tuple[str, Path, int, s
|
|
|
1790
1800
|
raise FileNotFoundError(
|
|
1791
1801
|
"No wrapper GDS found (expected one of gds/user_project_wrapper.gds[.gz], "
|
|
1792
1802
|
"gds/user_analog_project_wrapper.gds[.gz], "
|
|
1793
|
-
"gds/openframe_project_wrapper.gds[.gz]
|
|
1803
|
+
"gds/openframe_project_wrapper.gds[.gz], "
|
|
1804
|
+
"gds/double_wide_openframe_project_wrapper.gds[.gz])."
|
|
1794
1805
|
)
|
|
1795
1806
|
if len(hits) > 1:
|
|
1796
1807
|
paths = ", ".join(h[1] for h in hits)
|
|
@@ -1804,7 +1815,7 @@ def _collect_push_candidates(project_root: Path) -> List[Tuple[str, Path, int, s
|
|
|
1804
1815
|
(wrapper_rel, abs_wrapper, abs_wrapper.stat().st_size, "wrapper")
|
|
1805
1816
|
]
|
|
1806
1817
|
|
|
1807
|
-
if kind
|
|
1818
|
+
if not is_padframe_only(kind):
|
|
1808
1819
|
ud = project_root / USER_DEFINES_REL
|
|
1809
1820
|
if ud.is_file():
|
|
1810
1821
|
results.append((USER_DEFINES_REL, ud, ud.stat().st_size, "aux"))
|
|
@@ -2169,7 +2180,7 @@ def push(project_root, sftp_host, sftp_username, sftp_key, project_id, project_n
|
|
|
2169
2180
|
upload_map = {
|
|
2170
2181
|
".cf/project.json": project_json_path,
|
|
2171
2182
|
}
|
|
2172
|
-
if detected_type
|
|
2183
|
+
if not is_padframe_only(detected_type):
|
|
2173
2184
|
upload_map["verilog/rtl/user_defines.v"] = collected.get("verilog/rtl/user_defines.v")
|
|
2174
2185
|
|
|
2175
2186
|
# Add the appropriate GDS file based on what was collected
|
|
@@ -3156,8 +3167,9 @@ def setup(project_root, repo_owner, repo_name, branch, pdk, caravel_lite,
|
|
|
3156
3167
|
except (json.JSONDecodeError, IOError):
|
|
3157
3168
|
pass # Use default if we can't read it
|
|
3158
3169
|
|
|
3159
|
-
|
|
3160
|
-
|
|
3170
|
+
padframe_only = is_padframe_only(project_type)
|
|
3171
|
+
harness_repo = HARNESS_REPOS.get(project_type)
|
|
3172
|
+
|
|
3161
3173
|
had_errors = False
|
|
3162
3174
|
|
|
3163
3175
|
def _error_text(err):
|
|
@@ -3186,8 +3198,8 @@ def setup(project_root, repo_owner, repo_name, branch, pdk, caravel_lite,
|
|
|
3186
3198
|
# If in "only" mode, only install what's specified
|
|
3187
3199
|
# If not in "only" mode, install everything
|
|
3188
3200
|
install_caravel = only_caravel or not only_mode
|
|
3189
|
-
# MCW is not used
|
|
3190
|
-
install_mcw = (only_mcw or not only_mode) and not
|
|
3201
|
+
# MCW is not used by bare-padframe projects
|
|
3202
|
+
install_mcw = (only_mcw or not only_mode) and not padframe_only
|
|
3191
3203
|
install_openlane = only_openlane or not only_mode
|
|
3192
3204
|
install_pdk = only_pdk or not only_mode
|
|
3193
3205
|
install_timing = only_timing or not only_mode
|
|
@@ -3201,11 +3213,14 @@ def setup(project_root, repo_owner, repo_name, branch, pdk, caravel_lite,
|
|
|
3201
3213
|
f"Repository: [yellow]{repo_owner}/{repo_name}@{branch}[/yellow]",
|
|
3202
3214
|
f"PDK: [yellow]{pdk}[/yellow]",
|
|
3203
3215
|
f"Project type: [yellow]{project_type}[/yellow]",
|
|
3204
|
-
f"Caravel variant: [yellow]{'caravel-lite' if caravel_lite else 'caravel'}[/yellow]",
|
|
3205
3216
|
]
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3217
|
+
if harness_repo:
|
|
3218
|
+
config_lines.append(f"Harness: [yellow]{harness_repo}[/yellow]")
|
|
3219
|
+
else:
|
|
3220
|
+
config_lines.append(f"Caravel variant: [yellow]{'caravel-lite' if caravel_lite else 'caravel'}[/yellow]")
|
|
3221
|
+
|
|
3222
|
+
if padframe_only:
|
|
3223
|
+
config_lines.append(f"[dim]MCW not needed for {project_type} projects[/dim]")
|
|
3209
3224
|
|
|
3210
3225
|
if only_mode:
|
|
3211
3226
|
installing = []
|
|
@@ -3254,21 +3269,26 @@ def setup(project_root, repo_owner, repo_name, branch, pdk, caravel_lite,
|
|
|
3254
3269
|
deps_dir.mkdir(exist_ok=True)
|
|
3255
3270
|
console.print(f"[green]✓[/green] Dependencies directory ready at {deps_dir}")
|
|
3256
3271
|
|
|
3257
|
-
# Step 2: Install Caravel/Caravel-Lite
|
|
3272
|
+
# Step 2: Install Caravel/Caravel-Lite, or the project type's own harness
|
|
3258
3273
|
if install_caravel:
|
|
3259
|
-
console.print("\n[bold]Step 2:[/bold] Installing Caravel...")
|
|
3260
3274
|
caravel_dir = project_root_path / 'caravel'
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3275
|
+
if harness_repo:
|
|
3276
|
+
console.print("\n[bold]Step 2:[/bold] Installing harness...")
|
|
3277
|
+
caravel_name = harness_repo.rstrip('/').rsplit('/', 1)[-1]
|
|
3278
|
+
caravel_repo = harness_repo
|
|
3279
|
+
mpw_tag = harness_ref(project_type, versions)
|
|
3280
|
+
else:
|
|
3281
|
+
console.print("\n[bold]Step 2:[/bold] Installing Caravel...")
|
|
3282
|
+
caravel_name = 'caravel-lite' if caravel_lite else 'caravel'
|
|
3283
|
+
|
|
3284
|
+
# Determine MPW tag based on PDK
|
|
3285
|
+
if pdk not in mpw_tags:
|
|
3286
|
+
console.print(f"[red]✗[/red] PDK '{pdk}' not found in version configuration")
|
|
3287
|
+
console.print(f"[yellow]Available PDKs: {', '.join(mpw_tags.keys())}[/yellow]")
|
|
3288
|
+
raise click.Abort()
|
|
3289
|
+
mpw_tag = mpw_tags[pdk]
|
|
3290
|
+
|
|
3291
|
+
caravel_repo = f'https://github.com/chipfoundry/{caravel_name}'
|
|
3272
3292
|
|
|
3273
3293
|
# Check if already installed with correct version
|
|
3274
3294
|
is_correct_version, current_version = check_version_installed(caravel_dir, mpw_tag)
|
|
@@ -3305,10 +3325,9 @@ def setup(project_root, repo_owner, repo_name, branch, pdk, caravel_lite,
|
|
|
3305
3325
|
console.print(f"[dim]{e.stderr}[/dim]")
|
|
3306
3326
|
|
|
3307
3327
|
# Step 3: Install Management Core Wrapper
|
|
3308
|
-
|
|
3309
|
-
if only_mcw and is_openframe:
|
|
3328
|
+
if only_mcw and padframe_only:
|
|
3310
3329
|
console.print("\n[bold]Step 3:[/bold] Installing Management Core Wrapper...")
|
|
3311
|
-
console.print("[yellow]⚠[/yellow] MCW is not used for
|
|
3330
|
+
console.print(f"[yellow]⚠[/yellow] MCW is not used for {project_type} projects, skipping...")
|
|
3312
3331
|
elif install_mcw:
|
|
3313
3332
|
console.print("\n[bold]Step 3:[/bold] Installing Management Core Wrapper...")
|
|
3314
3333
|
mcw_dir = project_root_path / 'mgmt_core_wrapper'
|
|
@@ -3641,13 +3660,17 @@ def setup(project_root, repo_owner, repo_name, branch, pdk, caravel_lite,
|
|
|
3641
3660
|
capture_output=True
|
|
3642
3661
|
)
|
|
3643
3662
|
# Install from GitHub source (workaround: PyPI package is under efabless, we use chipfoundry repo)
|
|
3663
|
+
env = os.environ.copy()
|
|
3664
|
+
if sys.platform == 'darwin':
|
|
3665
|
+
env['LDSHARED'] = 'clang++ -dynamiclib -undefined dynamic_lookup'
|
|
3644
3666
|
subprocess.run(
|
|
3645
3667
|
[
|
|
3646
3668
|
venv_python, '-m', 'pip', 'install', '--upgrade', '--no-cache-dir',
|
|
3647
3669
|
'git+https://github.com/chipfoundry/caravel-sim-infrastructure.git@main#subdirectory=cocotb'
|
|
3648
3670
|
],
|
|
3649
3671
|
check=True,
|
|
3650
|
-
capture_output=True
|
|
3672
|
+
capture_output=True,
|
|
3673
|
+
env=env
|
|
3651
3674
|
)
|
|
3652
3675
|
console.print("[green]✓[/green] Cocotb environment set up successfully")
|
|
3653
3676
|
|
|
@@ -3738,6 +3761,8 @@ def setup(project_root, repo_owner, repo_name, branch, pdk, caravel_lite,
|
|
|
3738
3761
|
console.print("[bold green]Installation complete![/bold green]")
|
|
3739
3762
|
else:
|
|
3740
3763
|
console.print("[bold green]Setup complete![/bold green]")
|
|
3764
|
+
if had_errors:
|
|
3765
|
+
sys.exit(1)
|
|
3741
3766
|
|
|
3742
3767
|
|
|
3743
3768
|
def _is_nix_toolchain_noise_line(line: str) -> bool:
|
|
@@ -3945,6 +3970,181 @@ def _queue_and_maybe_poll_remote_job(
|
|
|
3945
3970
|
client.close()
|
|
3946
3971
|
|
|
3947
3972
|
|
|
3973
|
+
def _harden_double_wide(
|
|
3974
|
+
*,
|
|
3975
|
+
macro: str,
|
|
3976
|
+
macro_dir: Path,
|
|
3977
|
+
config_file: Path,
|
|
3978
|
+
project_root_path: Path,
|
|
3979
|
+
tag: Optional[str],
|
|
3980
|
+
from_step: Optional[str],
|
|
3981
|
+
open_in_openroad: bool,
|
|
3982
|
+
open_in_klayout: bool,
|
|
3983
|
+
pdk: Optional[str],
|
|
3984
|
+
use_docker: bool,
|
|
3985
|
+
dry_run: bool,
|
|
3986
|
+
list_from_steps: bool,
|
|
3987
|
+
):
|
|
3988
|
+
"""Harden a `double_wide` macro with LibreLane 3 (native PDN-to-pad routing).
|
|
3989
|
+
|
|
3990
|
+
double_wide bypasses the OpenLane-2/Docker-venv path used by every other
|
|
3991
|
+
project type: its upstream template ships its own `flake.nix` pinning
|
|
3992
|
+
LibreLane 3, whose native PDN generator (`PDN_ENABLE_PINS`) reaches the
|
|
3993
|
+
padframe-aligned power pins directly, so no bridging "connection" macro
|
|
3994
|
+
or cf-cli-managed OpenLane install is needed.
|
|
3995
|
+
"""
|
|
3996
|
+
gui_mode_count = int(open_in_openroad) + int(open_in_klayout)
|
|
3997
|
+
|
|
3998
|
+
if use_docker:
|
|
3999
|
+
console.print("[red]✗[/red] double_wide hardening does not support --use-docker")
|
|
4000
|
+
console.print(
|
|
4001
|
+
"[yellow]double_wide uses the project's own Nix flake (LibreLane 3). "
|
|
4002
|
+
"Install Nix: https://librelane.readthedocs.io[/yellow]"
|
|
4003
|
+
)
|
|
4004
|
+
raise click.Abort()
|
|
4005
|
+
|
|
4006
|
+
flake_dir = librelane_run.double_wide_flake_dir(project_root_path)
|
|
4007
|
+
if flake_dir is None:
|
|
4008
|
+
console.print(f"[red]✗[/red] No flake.nix found in {project_root_path}")
|
|
4009
|
+
console.print("[yellow]double_wide projects must ship a flake.nix pinning LibreLane 3[/yellow]")
|
|
4010
|
+
raise click.Abort()
|
|
4011
|
+
|
|
4012
|
+
if not librelane_run.double_wide_nix_available(flake_dir):
|
|
4013
|
+
console.print("[red]✗[/red] Nix not available or cannot resolve the project's LibreLane 3 flake")
|
|
4014
|
+
console.print("[yellow]Install Nix from: https://librelane.readthedocs.io[/yellow]")
|
|
4015
|
+
raise click.Abort()
|
|
4016
|
+
|
|
4017
|
+
pdk_root = librelane_run.pdk_root_dir(project_root_path)
|
|
4018
|
+
pdk = librelane_run.detect_pdk(project_root_path, pdk)
|
|
4019
|
+
|
|
4020
|
+
valid_from_steps, valid_from_steps_error = librelane_run.get_double_wide_valid_from_steps(
|
|
4021
|
+
flake_dir=flake_dir, config_file=config_file, working_dir=project_root_path,
|
|
4022
|
+
)
|
|
4023
|
+
if valid_from_steps_error:
|
|
4024
|
+
console.print("[red]✗[/red] Failed to load LibreLane 3 step list for this macro")
|
|
4025
|
+
console.print(f"[yellow]Error:[/yellow] {valid_from_steps_error}")
|
|
4026
|
+
sys.exit(1)
|
|
4027
|
+
|
|
4028
|
+
if list_from_steps:
|
|
4029
|
+
console.print(f"[bold cyan]Valid --from steps for {macro}:[/bold cyan]")
|
|
4030
|
+
if valid_from_steps:
|
|
4031
|
+
for step_name in valid_from_steps:
|
|
4032
|
+
console.print(f" • {step_name}")
|
|
4033
|
+
else:
|
|
4034
|
+
console.print("[yellow]No steps found for this flow[/yellow]")
|
|
4035
|
+
return
|
|
4036
|
+
|
|
4037
|
+
if from_step and from_step not in (valid_from_steps or []):
|
|
4038
|
+
console.print(f"[red]✗[/red] Invalid --from step: {from_step}")
|
|
4039
|
+
matches = difflib.get_close_matches(from_step, valid_from_steps or [], n=5, cutoff=0.4)
|
|
4040
|
+
if matches:
|
|
4041
|
+
console.print("[yellow]Did you mean:[/yellow]")
|
|
4042
|
+
for m in matches:
|
|
4043
|
+
console.print(f" • {m}")
|
|
4044
|
+
else:
|
|
4045
|
+
console.print(f"[yellow]Use 'cf harden {macro} --list-from-steps' to see valid step names[/yellow]")
|
|
4046
|
+
sys.exit(1)
|
|
4047
|
+
|
|
4048
|
+
auto_selected_latest_tag = False
|
|
4049
|
+
if (from_step or gui_mode_count) and not tag:
|
|
4050
|
+
runs_dir = macro_dir / 'runs'
|
|
4051
|
+
candidate_runs = [p for p in runs_dir.iterdir() if p.is_dir()] if runs_dir.exists() else []
|
|
4052
|
+
if not candidate_runs:
|
|
4053
|
+
console.print("[red]✗[/red] No existing runs found for this macro")
|
|
4054
|
+
if gui_mode_count:
|
|
4055
|
+
console.print("[yellow]Create a hardening run first, or specify --tag <existing_tag>[/yellow]")
|
|
4056
|
+
else:
|
|
4057
|
+
console.print("[yellow]Run without --from first, or specify --tag <existing_tag>[/yellow]")
|
|
4058
|
+
sys.exit(1)
|
|
4059
|
+
latest_run = max(candidate_runs, key=lambda p: p.stat().st_mtime)
|
|
4060
|
+
tag = latest_run.name
|
|
4061
|
+
auto_selected_latest_tag = True
|
|
4062
|
+
elif not tag:
|
|
4063
|
+
tag = librelane_run.new_run_tag()
|
|
4064
|
+
|
|
4065
|
+
console.print("\n" + "="*60)
|
|
4066
|
+
console.print(f"[bold cyan]Hardening: {macro}[/bold cyan] [dim](LibreLane 3, native PDN-to-pad routing)[/dim]")
|
|
4067
|
+
console.print(f"Config: [yellow]{config_file.name}[/yellow]")
|
|
4068
|
+
console.print(f"Run tag: [yellow]{tag}[/yellow]")
|
|
4069
|
+
if auto_selected_latest_tag:
|
|
4070
|
+
if gui_mode_count:
|
|
4071
|
+
console.print("[yellow]Using latest existing run tag (auto-selected because GUI mode was requested without --tag)[/yellow]")
|
|
4072
|
+
else:
|
|
4073
|
+
console.print("[yellow]Using latest existing run tag (auto-selected because --from was provided without --tag)[/yellow]")
|
|
4074
|
+
if from_step:
|
|
4075
|
+
console.print(f"Start from: [yellow]{from_step}[/yellow]")
|
|
4076
|
+
console.print("[yellow]Mode:[/yellow] resume from existing state under this tag (no overwrite)")
|
|
4077
|
+
if open_in_openroad:
|
|
4078
|
+
console.print("[yellow]Mode:[/yellow] open existing run in OpenROAD GUI")
|
|
4079
|
+
elif open_in_klayout:
|
|
4080
|
+
console.print("[yellow]Mode:[/yellow] open existing run in KLayout GUI")
|
|
4081
|
+
console.print(f"PDK: [yellow]{pdk}[/yellow]")
|
|
4082
|
+
console.print(f"PDK Root: [yellow]{pdk_root}[/yellow]")
|
|
4083
|
+
console.print("Execution: [yellow]Nix (project flake, LibreLane 3)[/yellow]")
|
|
4084
|
+
console.print("="*60 + "\n")
|
|
4085
|
+
|
|
4086
|
+
cmd, env = librelane_run.build_double_wide_librelane_command(
|
|
4087
|
+
project_root=project_root_path,
|
|
4088
|
+
flake_dir=flake_dir,
|
|
4089
|
+
config_file=config_file,
|
|
4090
|
+
pdk_root=pdk_root,
|
|
4091
|
+
pdk=pdk,
|
|
4092
|
+
tag=tag,
|
|
4093
|
+
from_step=from_step,
|
|
4094
|
+
open_in_openroad=open_in_openroad,
|
|
4095
|
+
open_in_klayout=open_in_klayout,
|
|
4096
|
+
)
|
|
4097
|
+
|
|
4098
|
+
if dry_run:
|
|
4099
|
+
console.print("[yellow]Dry run - command that would be executed:[/yellow]")
|
|
4100
|
+
console.print(" ".join(cmd))
|
|
4101
|
+
return
|
|
4102
|
+
|
|
4103
|
+
console.print(f"[cyan]Running LibreLane 3 via Nix on {macro}...[/cyan]")
|
|
4104
|
+
|
|
4105
|
+
process = None
|
|
4106
|
+
try:
|
|
4107
|
+
process = subprocess.Popen(
|
|
4108
|
+
cmd,
|
|
4109
|
+
cwd=str(project_root_path),
|
|
4110
|
+
env=env,
|
|
4111
|
+
preexec_fn=os.setsid if os.name != 'nt' else None,
|
|
4112
|
+
)
|
|
4113
|
+
returncode = process.wait()
|
|
4114
|
+
|
|
4115
|
+
if returncode == 0:
|
|
4116
|
+
console.print(f"\n[green]✓[/green] [bold green]Successfully hardened {macro}![/bold green]")
|
|
4117
|
+
console.print(f"[dim]Run directory: {macro_dir}/runs/{tag}/[/dim]")
|
|
4118
|
+
elif returncode == -2 or returncode == 130: # SIGINT
|
|
4119
|
+
console.print("\n[yellow]⚠[/yellow] Hardening interrupted by user")
|
|
4120
|
+
sys.exit(130)
|
|
4121
|
+
else:
|
|
4122
|
+
console.print(f"\n[red]✗[/red] [bold red]Hardening failed with exit code {returncode}[/bold red]")
|
|
4123
|
+
console.print(f"[yellow]Check logs in: {macro_dir}/runs/{tag}/[/yellow]")
|
|
4124
|
+
sys.exit(returncode)
|
|
4125
|
+
|
|
4126
|
+
except KeyboardInterrupt:
|
|
4127
|
+
console.print("\n[yellow]⚠[/yellow] Hardening interrupted by user")
|
|
4128
|
+
try:
|
|
4129
|
+
if process is not None:
|
|
4130
|
+
if os.name != 'nt':
|
|
4131
|
+
os.killpg(os.getpgid(process.pid), signal.SIGTERM)
|
|
4132
|
+
process.wait(timeout=5)
|
|
4133
|
+
else:
|
|
4134
|
+
process.terminate()
|
|
4135
|
+
process.wait(timeout=5)
|
|
4136
|
+
except Exception:
|
|
4137
|
+
if process is not None:
|
|
4138
|
+
if os.name != 'nt':
|
|
4139
|
+
os.killpg(os.getpgid(process.pid), signal.SIGKILL)
|
|
4140
|
+
else:
|
|
4141
|
+
process.kill()
|
|
4142
|
+
sys.exit(130)
|
|
4143
|
+
except Exception as e:
|
|
4144
|
+
console.print(f"\n[red]✗[/red] Error: {e}")
|
|
4145
|
+
sys.exit(1)
|
|
4146
|
+
|
|
4147
|
+
|
|
3948
4148
|
@main.command(
|
|
3949
4149
|
'harden',
|
|
3950
4150
|
cls=CategorizedCommand,
|
|
@@ -4002,6 +4202,11 @@ def harden(
|
|
|
4002
4202
|
):
|
|
4003
4203
|
"""Harden a macro using LibreLane (OpenLane 2).
|
|
4004
4204
|
|
|
4205
|
+
For `double_wide` projects, hardens the single flat wrapper design with
|
|
4206
|
+
LibreLane 3 instead (using the project's own pinned flake.nix), since its
|
|
4207
|
+
native PDN generator routes directly to the padframe-aligned power pins.
|
|
4208
|
+
Every other project type is unaffected and keeps using OpenLane 2.
|
|
4209
|
+
|
|
4005
4210
|
Examples:
|
|
4006
4211
|
cf harden user_proj_example
|
|
4007
4212
|
cf harden --list
|
|
@@ -4059,7 +4264,7 @@ def harden(
|
|
|
4059
4264
|
wait_timeout=wait_timeout,
|
|
4060
4265
|
label="Remote PNR",
|
|
4061
4266
|
)
|
|
4062
|
-
|
|
4267
|
+
sys.exit(1)
|
|
4063
4268
|
|
|
4064
4269
|
# Check if project is initialized (skip check for --list or when no macro specified, allow graceful return)
|
|
4065
4270
|
if not list_designs and macro:
|
|
@@ -4074,24 +4279,26 @@ def harden(
|
|
|
4074
4279
|
if not openlane_dir.exists():
|
|
4075
4280
|
console.print(f"[red]✗[/red] OpenLane directory not found: {openlane_dir}")
|
|
4076
4281
|
console.print("[yellow]Run 'cf setup' first to install OpenLane[/yellow]")
|
|
4077
|
-
|
|
4282
|
+
if list_designs:
|
|
4283
|
+
return
|
|
4284
|
+
sys.exit(1)
|
|
4078
4285
|
|
|
4079
4286
|
if list_from_steps and not macro:
|
|
4080
4287
|
console.print("[red]✗[/red] --list-from-steps requires a macro name")
|
|
4081
4288
|
console.print("[yellow]Example:[/yellow] cf harden user_proj_example --list-from-steps")
|
|
4082
|
-
|
|
4289
|
+
sys.exit(1)
|
|
4083
4290
|
|
|
4084
4291
|
gui_mode_count = int(open_in_openroad) + int(open_in_klayout)
|
|
4085
4292
|
if gui_mode_count > 1:
|
|
4086
4293
|
console.print("[red]✗[/red] Use only one GUI flag: --open-in-openroad or --open-in-klayout")
|
|
4087
|
-
|
|
4294
|
+
sys.exit(1)
|
|
4088
4295
|
if gui_mode_count and from_step:
|
|
4089
4296
|
console.print("[red]✗[/red] --from cannot be combined with GUI modes")
|
|
4090
4297
|
console.print("[yellow]Use --tag to select which run to open, or omit --tag to use latest run[/yellow]")
|
|
4091
|
-
|
|
4298
|
+
sys.exit(1)
|
|
4092
4299
|
if gui_mode_count and list_from_steps:
|
|
4093
4300
|
console.print("[red]✗[/red] --list-from-steps cannot be combined with GUI modes")
|
|
4094
|
-
|
|
4301
|
+
sys.exit(1)
|
|
4095
4302
|
|
|
4096
4303
|
# If no macro specified, show prompt with available macros
|
|
4097
4304
|
no_macro_specified = not macro and not list_designs and not list_from_steps
|
|
@@ -4123,7 +4330,7 @@ def harden(
|
|
|
4123
4330
|
if not macro_dir.exists():
|
|
4124
4331
|
console.print(f"[red]✗[/red] Macro not found: {macro}")
|
|
4125
4332
|
console.print(f"[yellow]Run 'cf harden --list' to see available macros[/yellow]")
|
|
4126
|
-
|
|
4333
|
+
sys.exit(1)
|
|
4127
4334
|
|
|
4128
4335
|
# Find config file
|
|
4129
4336
|
config_file = None
|
|
@@ -4136,8 +4343,29 @@ def harden(
|
|
|
4136
4343
|
if not config_file:
|
|
4137
4344
|
console.print(f"[red]✗[/red] No config file found for {macro}")
|
|
4138
4345
|
console.print(f"[yellow]Expected one of: config.json, config.yaml, config.tcl[/yellow]")
|
|
4346
|
+
sys.exit(1)
|
|
4347
|
+
|
|
4348
|
+
# `double_wide` is hardened as one flat design with LibreLane 3's native
|
|
4349
|
+
# PDN-to-pad routing, using the project's own pinned flake.nix instead of
|
|
4350
|
+
# cf-cli's OpenLane-2 Nix/Docker install. Every other project type falls
|
|
4351
|
+
# through to the existing OpenLane 2 (CI2511) flow below, unaffected.
|
|
4352
|
+
if read_project_type(project_root_path) == "double_wide":
|
|
4353
|
+
_harden_double_wide(
|
|
4354
|
+
macro=macro,
|
|
4355
|
+
macro_dir=macro_dir,
|
|
4356
|
+
config_file=Path(config_file),
|
|
4357
|
+
project_root_path=project_root_path,
|
|
4358
|
+
tag=tag,
|
|
4359
|
+
from_step=from_step,
|
|
4360
|
+
open_in_openroad=open_in_openroad,
|
|
4361
|
+
open_in_klayout=open_in_klayout,
|
|
4362
|
+
pdk=pdk,
|
|
4363
|
+
use_docker=use_docker,
|
|
4364
|
+
dry_run=dry_run,
|
|
4365
|
+
list_from_steps=list_from_steps,
|
|
4366
|
+
)
|
|
4139
4367
|
return
|
|
4140
|
-
|
|
4368
|
+
|
|
4141
4369
|
# Check for LibreLane venv
|
|
4142
4370
|
librelane_venv = openlane_dir / '.venv'
|
|
4143
4371
|
if not librelane_venv.exists():
|
|
@@ -4178,7 +4406,7 @@ def harden(
|
|
|
4178
4406
|
" continue\n"
|
|
4179
4407
|
" seen.add(step_id)\n"
|
|
4180
4408
|
" steps.append(step_id)\n"
|
|
4181
|
-
"print(json.dumps({'steps': steps}))\n"
|
|
4409
|
+
f"print({_STEPS_MARKER!r} + json.dumps({{'steps': steps}}))\n"
|
|
4182
4410
|
)
|
|
4183
4411
|
result = subprocess.run(
|
|
4184
4412
|
[str(librelane_python), '-c', script, macro_config],
|
|
@@ -4190,8 +4418,7 @@ def harden(
|
|
|
4190
4418
|
err = (result.stderr or result.stdout or 'unknown error').strip()
|
|
4191
4419
|
return None, err
|
|
4192
4420
|
try:
|
|
4193
|
-
|
|
4194
|
-
return payload.get('steps', []), None
|
|
4421
|
+
return _parse_librelane_steps(result.stdout), None
|
|
4195
4422
|
except Exception as exc:
|
|
4196
4423
|
return None, str(exc)
|
|
4197
4424
|
|
|
@@ -4206,7 +4433,7 @@ def harden(
|
|
|
4206
4433
|
console.print("[red]✗[/red] Failed to load LibreLane step list for this macro")
|
|
4207
4434
|
console.print(f"[yellow]Error:[/yellow] {valid_from_steps_error}")
|
|
4208
4435
|
console.print("[yellow]Run 'cf setup --only-openlane' to ensure LibreLane is installed correctly[/yellow]")
|
|
4209
|
-
|
|
4436
|
+
sys.exit(1)
|
|
4210
4437
|
|
|
4211
4438
|
if list_from_steps:
|
|
4212
4439
|
console.print(f"[bold cyan]Valid --from steps for {macro}:[/bold cyan]")
|
|
@@ -4226,7 +4453,7 @@ def harden(
|
|
|
4226
4453
|
console.print(f" • {m}")
|
|
4227
4454
|
else:
|
|
4228
4455
|
console.print(f"[yellow]Use 'cf harden {macro} --list-from-steps' to see valid step names[/yellow]")
|
|
4229
|
-
|
|
4456
|
+
sys.exit(1)
|
|
4230
4457
|
|
|
4231
4458
|
# Fetch versions from upstream
|
|
4232
4459
|
console.print("[dim]Fetching version information from cf-cli repository...[/dim]")
|
|
@@ -4250,7 +4477,7 @@ def harden(
|
|
|
4250
4477
|
# Check for conflicting flags
|
|
4251
4478
|
if force_nix_flag and force_docker_flag:
|
|
4252
4479
|
console.print("[red]✗[/red] Cannot use both --use-nix and --use-docker")
|
|
4253
|
-
|
|
4480
|
+
sys.exit(1)
|
|
4254
4481
|
|
|
4255
4482
|
# Check if Nix is available
|
|
4256
4483
|
if force_nix_flag or not force_docker_flag:
|
|
@@ -4321,7 +4548,7 @@ def harden(
|
|
|
4321
4548
|
if not pdk_dir.exists():
|
|
4322
4549
|
console.print(f"[red]✗[/red] PDK not found: {pdk_dir}")
|
|
4323
4550
|
console.print("[yellow]Run 'cf setup --only-pdk' to install the PDK[/yellow]")
|
|
4324
|
-
|
|
4551
|
+
sys.exit(1)
|
|
4325
4552
|
|
|
4326
4553
|
auto_selected_latest_tag = False
|
|
4327
4554
|
if (from_step or gui_mode_count) and not tag:
|
|
@@ -4332,7 +4559,7 @@ def harden(
|
|
|
4332
4559
|
console.print("[yellow]Create a hardening run first, or specify --tag <existing_tag>[/yellow]")
|
|
4333
4560
|
else:
|
|
4334
4561
|
console.print("[yellow]Run without --from first, or specify --tag <existing_tag>[/yellow]")
|
|
4335
|
-
|
|
4562
|
+
sys.exit(1)
|
|
4336
4563
|
candidate_runs = [p for p in runs_dir.iterdir() if p.is_dir()]
|
|
4337
4564
|
if not candidate_runs:
|
|
4338
4565
|
console.print("[red]✗[/red] No existing runs found for this macro")
|
|
@@ -4340,7 +4567,7 @@ def harden(
|
|
|
4340
4567
|
console.print("[yellow]Create a hardening run first, or specify --tag <existing_tag>[/yellow]")
|
|
4341
4568
|
else:
|
|
4342
4569
|
console.print("[yellow]Run without --from first, or specify --tag <existing_tag>[/yellow]")
|
|
4343
|
-
|
|
4570
|
+
sys.exit(1)
|
|
4344
4571
|
latest_run = max(candidate_runs, key=lambda p: p.stat().st_mtime)
|
|
4345
4572
|
tag = latest_run.name
|
|
4346
4573
|
auto_selected_latest_tag = True
|
|
@@ -4471,13 +4698,13 @@ def harden(
|
|
|
4471
4698
|
|
|
4472
4699
|
if returncode == 0:
|
|
4473
4700
|
console.print(f"\n[green]✓[/green] [bold green]Successfully hardened {macro}![/bold green]")
|
|
4474
|
-
console.print(f"[dim]
|
|
4701
|
+
console.print(f"[dim]Run directory: {macro_dir}/runs/{tag}/[/dim]")
|
|
4475
4702
|
elif returncode == -2 or returncode == 130: # SIGINT
|
|
4476
4703
|
console.print("\n[yellow]⚠[/yellow] Hardening interrupted by user")
|
|
4477
4704
|
sys.exit(130)
|
|
4478
4705
|
else:
|
|
4479
4706
|
console.print(f"\n[red]✗[/red] [bold red]Hardening failed with exit code {returncode}[/bold red]")
|
|
4480
|
-
console.print(f"[yellow]Check logs in: {
|
|
4707
|
+
console.print(f"[yellow]Check logs in: {macro_dir}/runs/{tag}/[/yellow]")
|
|
4481
4708
|
sys.exit(returncode)
|
|
4482
4709
|
|
|
4483
4710
|
except KeyboardInterrupt:
|
|
@@ -4495,8 +4722,10 @@ def harden(
|
|
|
4495
4722
|
os.killpg(os.getpgid(process.pid), signal.SIGKILL)
|
|
4496
4723
|
else:
|
|
4497
4724
|
process.kill()
|
|
4725
|
+
sys.exit(130)
|
|
4498
4726
|
except Exception as e:
|
|
4499
4727
|
console.print(f"\n[red]✗[/red] Error: {e}")
|
|
4728
|
+
sys.exit(1)
|
|
4500
4729
|
|
|
4501
4730
|
@main.group('repo')
|
|
4502
4731
|
def repo_group():
|
|
@@ -4860,7 +5089,7 @@ def precheck(project_root, skip_checks, magic_drc, checks, list_checks, dry_run,
|
|
|
4860
5089
|
project_data = json.load(f)
|
|
4861
5090
|
project_type = project_data.get('project', {}).get('type', 'digital')
|
|
4862
5091
|
|
|
4863
|
-
if project_type
|
|
5092
|
+
if not is_padframe_only(project_type):
|
|
4864
5093
|
gpio_config = get_gpio_config_from_project_json(str(project_json_path))
|
|
4865
5094
|
if not gpio_config or len(gpio_config) == 0:
|
|
4866
5095
|
console.print("[red]✗[/red] GPIO configuration not found in project.json")
|
|
@@ -4882,20 +5111,31 @@ def precheck(project_root, skip_checks, magic_drc, checks, list_checks, dry_run,
|
|
|
4882
5111
|
if not (pdk_root / pdk).exists():
|
|
4883
5112
|
console.print(f"[red]✗[/red] PDK not found at {pdk_root / pdk}")
|
|
4884
5113
|
console.print("[yellow]Run 'cf setup --only-pdk' to install[/yellow]")
|
|
4885
|
-
|
|
5114
|
+
sys.exit(1)
|
|
4886
5115
|
|
|
4887
5116
|
if shutil.which('docker') is None:
|
|
4888
5117
|
console.print("[red]✗[/red] Docker not found. Docker is required to run precheck.")
|
|
4889
|
-
|
|
5118
|
+
sys.exit(1)
|
|
4890
5119
|
|
|
4891
5120
|
pdk_path = pdk_root / pdk
|
|
4892
|
-
|
|
5121
|
+
|
|
5122
|
+
# The precheck image ships caravel at /opt/caravel. Other harnesses are
|
|
5123
|
+
# installed by `cf setup` into <project>/caravel, which is mounted below.
|
|
5124
|
+
golden_root = '/opt/caravel'
|
|
5125
|
+
if project_type in HARNESS_REPOS:
|
|
5126
|
+
harness_dir = project_root_path / 'caravel'
|
|
5127
|
+
if not harness_dir.is_dir():
|
|
5128
|
+
console.print(f"[red]✗[/red] {project_type} harness not found at {harness_dir}")
|
|
5129
|
+
console.print("[yellow]Run 'cf setup --only-caravel' to install it[/yellow]")
|
|
5130
|
+
sys.exit(1)
|
|
5131
|
+
golden_root = str(harness_dir.resolve())
|
|
5132
|
+
|
|
4893
5133
|
docker_image = 'chipfoundry/mpw_precheck:latest'
|
|
4894
5134
|
|
|
4895
5135
|
precheck_args = [
|
|
4896
5136
|
'-i', str(project_root_path),
|
|
4897
5137
|
'-p', str(pdk_path),
|
|
4898
|
-
'-c',
|
|
5138
|
+
'-c', golden_root,
|
|
4899
5139
|
]
|
|
4900
5140
|
|
|
4901
5141
|
if magic_drc:
|
|
@@ -4909,13 +5149,33 @@ def precheck(project_root, skip_checks, magic_drc, checks, list_checks, dry_run,
|
|
|
4909
5149
|
if skip_checks:
|
|
4910
5150
|
precheck_args.extend(['--skip-checks'] + list(skip_checks))
|
|
4911
5151
|
|
|
4912
|
-
|
|
5152
|
+
# CF_PRECHECK_SOURCE points at a local cf-precheck checkout to test
|
|
5153
|
+
# unreleased changes; otherwise the latest release is installed from PyPI.
|
|
5154
|
+
precheck_source = os.environ.get('CF_PRECHECK_SOURCE')
|
|
5155
|
+
if precheck_source:
|
|
5156
|
+
precheck_source = str(Path(precheck_source).expanduser().resolve())
|
|
5157
|
+
if not (Path(precheck_source) / 'pyproject.toml').is_file():
|
|
5158
|
+
console.print(f"[red]✗[/red] CF_PRECHECK_SOURCE is not a cf-precheck checkout: {precheck_source}")
|
|
5159
|
+
sys.exit(1)
|
|
5160
|
+
install_cmd = (
|
|
5161
|
+
f'cp -r {shlex.quote(precheck_source)} /tmp/cf-precheck-src && '
|
|
5162
|
+
'pip3 install -q --root-user-action=ignore /tmp/cf-precheck-src'
|
|
5163
|
+
)
|
|
5164
|
+
else:
|
|
5165
|
+
install_cmd = 'pip3 install --upgrade -q --root-user-action=ignore cf-precheck 2>/dev/null'
|
|
5166
|
+
inner_cmd = install_cmd + ' && exec cf-precheck ' + ' '.join(precheck_args)
|
|
4913
5167
|
|
|
4914
5168
|
docker_cmd = [
|
|
4915
5169
|
'docker', 'run', '--rm', '--init',
|
|
4916
5170
|
'--platform', 'linux/amd64',
|
|
4917
5171
|
'-v', f'{project_root_path}:{project_root_path}',
|
|
4918
5172
|
'-v', f'{pdk_root}:{pdk_root}',
|
|
5173
|
+
]
|
|
5174
|
+
if golden_root != '/opt/caravel' and not golden_root.startswith(f'{project_root_path}/'):
|
|
5175
|
+
docker_cmd += ['-v', f'{golden_root}:{golden_root}']
|
|
5176
|
+
if precheck_source:
|
|
5177
|
+
docker_cmd += ['-v', f'{precheck_source}:{precheck_source}:ro']
|
|
5178
|
+
docker_cmd += [
|
|
4919
5179
|
'-e', f'PDK_ROOT={pdk_root}',
|
|
4920
5180
|
'-e', f'PDK_PATH={pdk_path}',
|
|
4921
5181
|
'-e', f'PDKPATH={pdk_path}',
|
|
@@ -4928,6 +5188,8 @@ def precheck(project_root, skip_checks, magic_drc, checks, list_checks, dry_run,
|
|
|
4928
5188
|
console.print("[bold cyan]CF Precheck[/bold cyan]")
|
|
4929
5189
|
console.print(f"Project: [yellow]{project_root_path}[/yellow]")
|
|
4930
5190
|
console.print(f"PDK: [yellow]{pdk}[/yellow]")
|
|
5191
|
+
if precheck_source:
|
|
5192
|
+
console.print(f"cf-precheck: [yellow]{precheck_source}[/yellow] (CF_PRECHECK_SOURCE)")
|
|
4931
5193
|
if skip_checks:
|
|
4932
5194
|
console.print(f"Skipping: [yellow]{', '.join(skip_checks)}[/yellow]")
|
|
4933
5195
|
if magic_drc:
|
|
@@ -4957,7 +5219,7 @@ def precheck(project_root, skip_checks, magic_drc, checks, list_checks, dry_run,
|
|
|
4957
5219
|
)
|
|
4958
5220
|
if result.returncode != 0:
|
|
4959
5221
|
console.print(f"[red]✗[/red] Docker image '{docker_image}' not found. Run 'cf setup --only-precheck' or check your connection.")
|
|
4960
|
-
|
|
5222
|
+
sys.exit(1)
|
|
4961
5223
|
console.print("[yellow]⚠[/yellow] Could not check for image updates (using cached image)")
|
|
4962
5224
|
|
|
4963
5225
|
console.print("[cyan]Running cf-precheck...[/cyan]\n")
|
|
@@ -4990,6 +5252,35 @@ def precheck(project_root, skip_checks, magic_drc, checks, list_checks, dry_run,
|
|
|
4990
5252
|
sys.exit(130)
|
|
4991
5253
|
except Exception as e:
|
|
4992
5254
|
console.print(f"\n[red]✗[/red] Error running precheck: {e}")
|
|
5255
|
+
sys.exit(1)
|
|
5256
|
+
|
|
5257
|
+
_STEPS_MARKER = 'CF_STEPS_JSON='
|
|
5258
|
+
|
|
5259
|
+
|
|
5260
|
+
def _parse_librelane_steps(stdout: str):
|
|
5261
|
+
"""Extract the step list printed by the `cf harden` step-list probe."""
|
|
5262
|
+
# LibreLane's console may emit terminal control sequences around the
|
|
5263
|
+
# payload (e.g. a show-cursor escape at exit), so locate it by marker.
|
|
5264
|
+
for line in stdout.splitlines():
|
|
5265
|
+
idx = line.find(_STEPS_MARKER)
|
|
5266
|
+
if idx != -1:
|
|
5267
|
+
payload, _ = json.JSONDecoder().raw_decode(line[idx + len(_STEPS_MARKER):])
|
|
5268
|
+
return payload.get('steps', [])
|
|
5269
|
+
raise ValueError('step list not found in LibreLane output')
|
|
5270
|
+
|
|
5271
|
+
|
|
5272
|
+
def _cocotb_run_results(run_dir: Path):
|
|
5273
|
+
"""Return (passed, failed) test directory names for a caravel_cocotb run."""
|
|
5274
|
+
passed, failed = [], []
|
|
5275
|
+
if not run_dir.is_dir():
|
|
5276
|
+
return passed, failed
|
|
5277
|
+
for test_dir in sorted(p for p in run_dir.iterdir() if p.is_dir()):
|
|
5278
|
+
if (test_dir / 'failed').exists():
|
|
5279
|
+
failed.append(test_dir.name)
|
|
5280
|
+
elif (test_dir / 'passed').exists():
|
|
5281
|
+
passed.append(test_dir.name)
|
|
5282
|
+
return passed, failed
|
|
5283
|
+
|
|
4993
5284
|
|
|
4994
5285
|
@main.command('verify')
|
|
4995
5286
|
@click.argument('test', required=False)
|
|
@@ -5090,14 +5381,16 @@ def verify(test, project_root, sim, list_tests, run_all, tag, dry_run, remote, p
|
|
|
5090
5381
|
|
|
5091
5382
|
project_json_path = project_root_path / '.cf' / 'project.json'
|
|
5092
5383
|
|
|
5093
|
-
# Get project type (needed for openframe flag)
|
|
5094
5384
|
with open(project_json_path, 'r') as f:
|
|
5095
5385
|
project_data = json.load(f)
|
|
5096
5386
|
project_type = project_data.get('project', {}).get('type', 'digital')
|
|
5097
|
-
|
|
5098
|
-
#
|
|
5387
|
+
# Bare-padframe projects run caravel_cocotb in OpenFrame mode against the
|
|
5388
|
+
# harness in <project>/caravel (its toplevel_cocotb.v sets the pad count).
|
|
5389
|
+
padframe_only = is_padframe_only(project_type)
|
|
5390
|
+
|
|
5391
|
+
# Check if GPIO configuration exists (skip check if just listing tests or padframe-only)
|
|
5099
5392
|
if not list_tests:
|
|
5100
|
-
if
|
|
5393
|
+
if not padframe_only:
|
|
5101
5394
|
gpio_config = get_gpio_config_from_project_json(str(project_json_path))
|
|
5102
5395
|
if not gpio_config or len(gpio_config) == 0:
|
|
5103
5396
|
console.print("[red]✗[/red] GPIO configuration not found in project.json")
|
|
@@ -5197,7 +5490,7 @@ def verify(test, project_root, sim, list_tests, run_all, tag, dry_run, remote, p
|
|
|
5197
5490
|
|
|
5198
5491
|
if dry_run:
|
|
5199
5492
|
console.print("[bold yellow]Dry run - configuration ready[/bold yellow]\n")
|
|
5200
|
-
openframe_flag = " --openframe" if
|
|
5493
|
+
openframe_flag = " --openframe" if padframe_only else ""
|
|
5201
5494
|
if test:
|
|
5202
5495
|
console.print(f"Would run: {caravel_cocotb_bin} -t {test} -sim {sim_arg}{openframe_flag}")
|
|
5203
5496
|
elif run_all:
|
|
@@ -5257,12 +5550,15 @@ def verify(test, project_root, sim, list_tests, run_all, tag, dry_run, remote, p
|
|
|
5257
5550
|
if sim.lower() == 'gl':
|
|
5258
5551
|
cmd.extend(['-sim', 'GL'])
|
|
5259
5552
|
|
|
5260
|
-
|
|
5261
|
-
if project_type == 'openframe':
|
|
5553
|
+
if padframe_only:
|
|
5262
5554
|
cmd.append('--openframe')
|
|
5263
5555
|
|
|
5264
5556
|
# Add CI flag to disable Docker interactive mode (required when not running in a terminal)
|
|
5265
5557
|
cmd.append('--CI')
|
|
5558
|
+
|
|
5559
|
+
from datetime import datetime
|
|
5560
|
+
run_tag = f"cf_verify_{datetime.now().strftime('%Y%m%d_%H%M%S_%f')}"
|
|
5561
|
+
cmd.extend(['-tag', run_tag])
|
|
5266
5562
|
|
|
5267
5563
|
# Run cocotb tests
|
|
5268
5564
|
console.print(f"[cyan]Running cocotb verification...[/cyan]")
|
|
@@ -5278,9 +5574,20 @@ def verify(test, project_root, sim, list_tests, run_all, tag, dry_run, remote, p
|
|
|
5278
5574
|
|
|
5279
5575
|
# Wait for process to complete
|
|
5280
5576
|
returncode = process.wait()
|
|
5281
|
-
|
|
5577
|
+
|
|
5282
5578
|
if returncode == 0:
|
|
5283
|
-
|
|
5579
|
+
# caravel_cocotb exits 0 even when tests fail; it records the
|
|
5580
|
+
# outcome as a 'passed' / 'failed' marker file in each test dir.
|
|
5581
|
+
passed, failed = _cocotb_run_results(cocotb_dir / 'sim' / run_tag)
|
|
5582
|
+
if failed or not passed:
|
|
5583
|
+
console.print(
|
|
5584
|
+
f"\n[red]✗[/red] Verification failed: {len(failed)} failed, {len(passed)} passed"
|
|
5585
|
+
)
|
|
5586
|
+
for name in failed:
|
|
5587
|
+
console.print(f" [red]•[/red] {name}")
|
|
5588
|
+
console.print(f"[yellow]Check logs in: {cocotb_dir / 'sim' / run_tag}[/yellow]")
|
|
5589
|
+
sys.exit(1)
|
|
5590
|
+
console.print(f"\n[green]✓[/green] Verification passed! ({len(passed)} test(s))")
|
|
5284
5591
|
elif returncode == -2 or returncode == 130: # SIGINT
|
|
5285
5592
|
console.print("\n[yellow]⚠[/yellow] Verification interrupted by user")
|
|
5286
5593
|
sys.exit(130)
|
|
@@ -5304,8 +5611,10 @@ def verify(test, project_root, sim, list_tests, run_all, tag, dry_run, remote, p
|
|
|
5304
5611
|
os.killpg(os.getpgid(process.pid), signal.SIGKILL)
|
|
5305
5612
|
else:
|
|
5306
5613
|
process.kill()
|
|
5614
|
+
sys.exit(130)
|
|
5307
5615
|
except Exception as e:
|
|
5308
5616
|
console.print(f"\n[red]✗[/red] Error: {e}")
|
|
5617
|
+
sys.exit(1)
|
|
5309
5618
|
|
|
5310
5619
|
|
|
5311
5620
|
DEFAULT_API_URL = 'https://api.chipfoundry.io'
|
|
@@ -22,8 +22,10 @@ _GDS_BASES: Tuple[Tuple[str, str], ...] = (
|
|
|
22
22
|
("analog", "gds/user_analog_project_wrapper"),
|
|
23
23
|
("digital", "gds/user_project_wrapper"),
|
|
24
24
|
("openframe", "gds/openframe_project_wrapper"),
|
|
25
|
+
("double_wide", "gds/double_wide_openframe_project_wrapper"),
|
|
25
26
|
("mini", "gds/user_project_wrapper_mini4"),
|
|
26
27
|
)
|
|
28
|
+
_PADFRAME_ONLY_KINDS = frozenset({"openframe", "double_wide"})
|
|
27
29
|
_GDS_SUFFIXES: Tuple[str, ...] = (".gds", ".gds.gz")
|
|
28
30
|
|
|
29
31
|
USER_DEFINES_REL = "verilog/rtl/user_defines.v"
|
|
@@ -301,7 +303,8 @@ def _push_critical_paths(repo: Path, project_json: Path) -> Set[str]:
|
|
|
301
303
|
f"{e} "
|
|
302
304
|
"Check that the wrapper GDS is committed and located under the "
|
|
303
305
|
"expected path (e.g. gds/user_project_wrapper.gds, "
|
|
304
|
-
"gds/openframe_project_wrapper.gds,
|
|
306
|
+
"gds/openframe_project_wrapper.gds, "
|
|
307
|
+
"gds/double_wide_openframe_project_wrapper.gds, etc.). If you use Git LFS, "
|
|
305
308
|
"run `git lfs pull` so the actual file (not the pointer) is present."
|
|
306
309
|
) from e
|
|
307
310
|
out: Set[str] = {gds_rel}
|
|
@@ -313,8 +316,8 @@ def _push_critical_paths(repo: Path, project_json: Path) -> Set[str]:
|
|
|
313
316
|
"Fix project type or GDS layout before remote push."
|
|
314
317
|
)
|
|
315
318
|
|
|
316
|
-
# user_defines.v is required except for
|
|
317
|
-
if kind_gds
|
|
319
|
+
# user_defines.v is required except for bare-padframe projects, matching collect_project_files().
|
|
320
|
+
if kind_gds not in _PADFRAME_ONLY_KINDS:
|
|
318
321
|
ud = repo / USER_DEFINES_REL
|
|
319
322
|
if ud.is_file() or _path_tracked_in_git(repo, USER_DEFINES_REL):
|
|
320
323
|
out.add(USER_DEFINES_REL)
|
|
@@ -24,8 +24,34 @@ GDS_TYPE_MAP = {
|
|
|
24
24
|
'user_analog_project_wrapper.gds.gz': 'analog',
|
|
25
25
|
'openframe_project_wrapper.gds': 'openframe',
|
|
26
26
|
'openframe_project_wrapper.gds.gz': 'openframe',
|
|
27
|
+
'double_wide_openframe_project_wrapper.gds': 'double_wide',
|
|
28
|
+
'double_wide_openframe_project_wrapper.gds.gz': 'double_wide',
|
|
27
29
|
}
|
|
28
30
|
|
|
31
|
+
PROJECT_TYPES: Tuple[str, ...] = ("digital", "analog", "openframe", "double_wide")
|
|
32
|
+
|
|
33
|
+
# Bare-padframe project types: no management SoC, no user_defines.v and no
|
|
34
|
+
# GPIO configuration. The user wrapper drives every pad control signal.
|
|
35
|
+
PADFRAME_ONLY_TYPES = frozenset({"openframe", "double_wide"})
|
|
36
|
+
|
|
37
|
+
# Project types whose harness is not caravel / caravel-lite. `cf setup` clones
|
|
38
|
+
# the harness into <project>/caravel so CARAVEL_ROOT keeps working for cocotb
|
|
39
|
+
# and precheck.
|
|
40
|
+
HARNESS_REPOS: Dict[str, str] = {
|
|
41
|
+
"double_wide": "https://github.com/chipfoundry/double_wide_openframe",
|
|
42
|
+
}
|
|
43
|
+
DEFAULT_HARNESS_REF = "main"
|
|
44
|
+
|
|
45
|
+
# `double_wide` is hardened as one flat design (no separate macros), and its
|
|
46
|
+
# upstream template ships its own flake.nix pinning LibreLane 3: the native
|
|
47
|
+
# PDN generator reaches the padframe-aligned power pins directly, so no
|
|
48
|
+
# bridging "connection" macro is needed. Every other project type keeps using
|
|
49
|
+
# `openlane_version` (LibreLane/OpenLane 2, fetched from cf-cli's versions.json).
|
|
50
|
+
# Pin the exact commit (not the `3.1.0.dev2` tag) so `nix` resolves it from the
|
|
51
|
+
# local store/cache without an unauthenticated GitHub API call that can 403.
|
|
52
|
+
DOUBLE_WIDE_LIBRELANE_REPO = "github:librelane/librelane"
|
|
53
|
+
DOUBLE_WIDE_LIBRELANE_REV = "f18a07aabb2dd3d6a7e3acc8c3b3621baa784a08" # 3.1.0.dev2
|
|
54
|
+
|
|
29
55
|
# Canonical GDS wrapper layouts used by remote precheck and remote push.
|
|
30
56
|
# Each entry is (project_kind, base path without suffix). Suffixes below.
|
|
31
57
|
# Keep in sync with chipignite-backend-services/src/precheck_service and
|
|
@@ -34,9 +60,35 @@ GDS_WRAPPER_BASES: Tuple[Tuple[str, str], ...] = (
|
|
|
34
60
|
("analog", "gds/user_analog_project_wrapper"),
|
|
35
61
|
("digital", "gds/user_project_wrapper"),
|
|
36
62
|
("openframe", "gds/openframe_project_wrapper"),
|
|
63
|
+
("double_wide", "gds/double_wide_openframe_project_wrapper"),
|
|
37
64
|
)
|
|
38
65
|
GDS_WRAPPER_SUFFIXES: Tuple[str, ...] = (".gds", ".gds.gz")
|
|
39
66
|
|
|
67
|
+
|
|
68
|
+
def is_padframe_only(project_type: Optional[str]) -> bool:
|
|
69
|
+
"""True for project types that have no management SoC or user_defines.v."""
|
|
70
|
+
return project_type in PADFRAME_ONLY_TYPES
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def read_project_type(project_root: Path, default: str = "digital") -> str:
|
|
74
|
+
"""Read `project.type` from `.cf/project.json`, defaulting if missing/unreadable."""
|
|
75
|
+
project_json_path = Path(project_root) / ".cf" / "project.json"
|
|
76
|
+
if project_json_path.is_file():
|
|
77
|
+
try:
|
|
78
|
+
data = json.loads(project_json_path.read_text())
|
|
79
|
+
project_type = data.get("project", {}).get("type")
|
|
80
|
+
if isinstance(project_type, str) and project_type.strip():
|
|
81
|
+
return project_type.strip()
|
|
82
|
+
except (json.JSONDecodeError, IOError):
|
|
83
|
+
pass
|
|
84
|
+
return default
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def harness_ref(project_type: str, versions: Optional[dict] = None) -> str:
|
|
88
|
+
"""Git ref of the harness that `cf setup` installs for ``project_type``."""
|
|
89
|
+
refs = (versions or {}).get("harness_refs") or {}
|
|
90
|
+
return refs.get(project_type) or DEFAULT_HARNESS_REF
|
|
91
|
+
|
|
40
92
|
USER_DEFINES_REL = "verilog/rtl/user_defines.v"
|
|
41
93
|
CF_PROJECT_JSON_REL = ".cf/project.json"
|
|
42
94
|
|
|
@@ -89,7 +141,7 @@ def collect_project_files(project_root: str) -> Dict[str, Optional[str]]:
|
|
|
89
141
|
Collect required project files from the given project_root.
|
|
90
142
|
Returns a dict mapping logical names to absolute file paths (or None if not found and optional).
|
|
91
143
|
Raises FileNotFoundError if any required file is missing.
|
|
92
|
-
Openframe projects do not require user_defines.v.
|
|
144
|
+
Openframe and double-wide projects do not require user_defines.v.
|
|
93
145
|
"""
|
|
94
146
|
project_root = Path(project_root)
|
|
95
147
|
collected = {}
|
|
@@ -158,13 +210,12 @@ def collect_project_files(project_root: str) -> Dict[str, Optional[str]]:
|
|
|
158
210
|
gds_name, gds_path = gds_file_to_use
|
|
159
211
|
collected[f"gds/{gds_name}"] = gds_path
|
|
160
212
|
|
|
161
|
-
# Collect standard required files (user_defines.v not required for openframe)
|
|
162
213
|
cf_json_path = project_root / ".cf" / "project.json"
|
|
163
214
|
collected[".cf/project.json"] = str(cf_json_path) if cf_json_path.exists() else None
|
|
164
215
|
|
|
165
216
|
user_defines_path = project_root / "verilog" / "rtl" / "user_defines.v"
|
|
166
|
-
if project_type
|
|
167
|
-
collected["verilog/rtl/user_defines.v"] = None
|
|
217
|
+
if is_padframe_only(project_type):
|
|
218
|
+
collected["verilog/rtl/user_defines.v"] = None
|
|
168
219
|
else:
|
|
169
220
|
if user_defines_path.exists():
|
|
170
221
|
collected["verilog/rtl/user_defines.v"] = str(user_defines_path)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|