chipfoundry-cli 2.5.4__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.5.4/README.md → chipfoundry_cli-2.7.0/PKG-INFO +54 -5
- chipfoundry_cli-2.5.4/PKG-INFO → chipfoundry_cli-2.7.0/README.md +24 -32
- {chipfoundry_cli-2.5.4 → chipfoundry_cli-2.7.0}/chipfoundry_cli/__init__.py +1 -1
- chipfoundry_cli-2.7.0/chipfoundry_cli/librelane_run.py +434 -0
- {chipfoundry_cli-2.5.4 → chipfoundry_cli-2.7.0}/chipfoundry_cli/main.py +394 -75
- chipfoundry_cli-2.7.0/chipfoundry_cli/preview.py +920 -0
- {chipfoundry_cli-2.5.4 → chipfoundry_cli-2.7.0}/chipfoundry_cli/remote_precheck_git.py +6 -3
- {chipfoundry_cli-2.5.4 → chipfoundry_cli-2.7.0}/chipfoundry_cli/utils.py +55 -4
- {chipfoundry_cli-2.5.4 → chipfoundry_cli-2.7.0}/pyproject.toml +1 -1
- {chipfoundry_cli-2.5.4 → chipfoundry_cli-2.7.0}/LICENSE +0 -0
- {chipfoundry_cli-2.5.4 → chipfoundry_cli-2.7.0}/chipfoundry_cli/check_refs.py +0 -0
- {chipfoundry_cli-2.5.4 → 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.5.4
|
|
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"
|
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
"""Shared LibreLane invocation helpers for `cf harden` and `cf preview`."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import shutil
|
|
8
|
+
import subprocess
|
|
9
|
+
import sys
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Dict, List, Optional, Sequence, Tuple
|
|
13
|
+
|
|
14
|
+
CONFIG_EXTS = ("json", "yaml", "yml", "tcl")
|
|
15
|
+
OPENLANE_DIR_NAMES = ("openlane", "librelane")
|
|
16
|
+
SYNTH_TO_STEP = "Yosys.Synthesis"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def find_flow_root(project_root: Path) -> Optional[Path]:
|
|
20
|
+
"""Return the first existing openlane/ or librelane/ directory."""
|
|
21
|
+
for name in OPENLANE_DIR_NAMES:
|
|
22
|
+
candidate = project_root / name
|
|
23
|
+
if candidate.is_dir():
|
|
24
|
+
return candidate
|
|
25
|
+
return None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def find_config_file(macro_dir: Path) -> Optional[Path]:
|
|
29
|
+
for ext in CONFIG_EXTS:
|
|
30
|
+
path = macro_dir / f"config.{ext}"
|
|
31
|
+
if path.is_file():
|
|
32
|
+
return path
|
|
33
|
+
return None
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def list_macro_configs(project_root: Path) -> List[Dict[str, str]]:
|
|
37
|
+
"""Discover LibreLane macros as ``{name, config_path, flow_root}``."""
|
|
38
|
+
found: List[Dict[str, str]] = []
|
|
39
|
+
seen = set()
|
|
40
|
+
for dir_name in OPENLANE_DIR_NAMES:
|
|
41
|
+
flow_root = project_root / dir_name
|
|
42
|
+
if not flow_root.is_dir():
|
|
43
|
+
continue
|
|
44
|
+
for child in sorted(flow_root.iterdir()):
|
|
45
|
+
if not child.is_dir() or child.name.startswith("."):
|
|
46
|
+
continue
|
|
47
|
+
config = find_config_file(child)
|
|
48
|
+
if config is None:
|
|
49
|
+
continue
|
|
50
|
+
rel = str(config.relative_to(project_root))
|
|
51
|
+
if child.name in seen:
|
|
52
|
+
continue
|
|
53
|
+
seen.add(child.name)
|
|
54
|
+
found.append(
|
|
55
|
+
{
|
|
56
|
+
"name": child.name,
|
|
57
|
+
"config_path": rel,
|
|
58
|
+
"flow_root": dir_name,
|
|
59
|
+
}
|
|
60
|
+
)
|
|
61
|
+
return found
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def detect_pdk(project_root: Path, pdk_override: Optional[str] = None) -> str:
|
|
65
|
+
if pdk_override:
|
|
66
|
+
return pdk_override
|
|
67
|
+
project_json = project_root / ".cf" / "project.json"
|
|
68
|
+
if project_json.is_file():
|
|
69
|
+
data = json.loads(project_json.read_text())
|
|
70
|
+
pdk = data.get("pdk")
|
|
71
|
+
if isinstance(pdk, str) and pdk.strip():
|
|
72
|
+
return pdk.strip()
|
|
73
|
+
return "sky130A"
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def pdk_root_dir(project_root: Path) -> Path:
|
|
77
|
+
return project_root / "dependencies" / "pdks"
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def librelane_venv(flow_root: Path) -> Path:
|
|
81
|
+
return flow_root / ".venv"
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def resolve_execution_backend(
|
|
85
|
+
*,
|
|
86
|
+
openlane_version: str,
|
|
87
|
+
force_nix: bool = False,
|
|
88
|
+
force_docker: bool = False,
|
|
89
|
+
) -> Tuple[str, Optional[str]]:
|
|
90
|
+
"""Return ``(\"nix\"|\"docker\", error)``."""
|
|
91
|
+
if force_nix and force_docker:
|
|
92
|
+
return "", "Cannot use both --use-nix and --use-docker"
|
|
93
|
+
|
|
94
|
+
use_nix = False
|
|
95
|
+
if force_nix or not force_docker:
|
|
96
|
+
if shutil.which("nix") is not None:
|
|
97
|
+
try:
|
|
98
|
+
result = subprocess.run(
|
|
99
|
+
[
|
|
100
|
+
"nix",
|
|
101
|
+
"flake",
|
|
102
|
+
"metadata",
|
|
103
|
+
f"github:chipfoundry/openlane-2/{openlane_version}",
|
|
104
|
+
"--json",
|
|
105
|
+
],
|
|
106
|
+
capture_output=True,
|
|
107
|
+
timeout=5,
|
|
108
|
+
)
|
|
109
|
+
use_nix = result.returncode == 0
|
|
110
|
+
except (OSError, subprocess.TimeoutExpired):
|
|
111
|
+
use_nix = False
|
|
112
|
+
if force_nix and not use_nix:
|
|
113
|
+
return "", "Nix not available or cannot access LibreLane flake"
|
|
114
|
+
|
|
115
|
+
if not use_nix:
|
|
116
|
+
use_docker = False
|
|
117
|
+
try:
|
|
118
|
+
result = subprocess.run(
|
|
119
|
+
["docker", "info"],
|
|
120
|
+
capture_output=True,
|
|
121
|
+
timeout=5,
|
|
122
|
+
)
|
|
123
|
+
use_docker = result.returncode == 0
|
|
124
|
+
except (OSError, subprocess.TimeoutExpired):
|
|
125
|
+
use_docker = False
|
|
126
|
+
if force_docker and not use_docker:
|
|
127
|
+
return "", "Docker not available"
|
|
128
|
+
if use_docker:
|
|
129
|
+
return "docker", None
|
|
130
|
+
if not force_nix:
|
|
131
|
+
return "", "Neither Nix nor Docker is available"
|
|
132
|
+
|
|
133
|
+
return "nix", None
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def build_librelane_command(
|
|
137
|
+
*,
|
|
138
|
+
project_root: Path,
|
|
139
|
+
flow_root: Path,
|
|
140
|
+
config_file: Path,
|
|
141
|
+
pdk: str,
|
|
142
|
+
pdk_root: Path,
|
|
143
|
+
tag: str,
|
|
144
|
+
openlane_version: str,
|
|
145
|
+
backend: str,
|
|
146
|
+
from_step: Optional[str] = None,
|
|
147
|
+
to_step: Optional[str] = None,
|
|
148
|
+
overwrite: bool = True,
|
|
149
|
+
) -> Tuple[List[str], Dict[str, str]]:
|
|
150
|
+
"""Build a Nix or Docker LibreLane command. Does not include GUI flows."""
|
|
151
|
+
env = os.environ.copy()
|
|
152
|
+
env.update(
|
|
153
|
+
{
|
|
154
|
+
"PROJECT_ROOT": str(project_root),
|
|
155
|
+
"PDK_ROOT": str(pdk_root),
|
|
156
|
+
"PDK": pdk,
|
|
157
|
+
"LIBRELANE_RUN_TAG": tag,
|
|
158
|
+
}
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
if backend == "nix":
|
|
162
|
+
cmd = [
|
|
163
|
+
"nix",
|
|
164
|
+
"run",
|
|
165
|
+
f"github:chipfoundry/openlane-2/{openlane_version}",
|
|
166
|
+
"--",
|
|
167
|
+
"--manual-pdk",
|
|
168
|
+
"--pdk-root",
|
|
169
|
+
str(pdk_root),
|
|
170
|
+
"--pdk",
|
|
171
|
+
pdk,
|
|
172
|
+
"--ef-save-views-to",
|
|
173
|
+
str(project_root),
|
|
174
|
+
"--run-tag",
|
|
175
|
+
tag,
|
|
176
|
+
]
|
|
177
|
+
else:
|
|
178
|
+
venv_bin = librelane_venv(flow_root) / "bin"
|
|
179
|
+
env["PATH"] = f"{venv_bin}:{env.get('PATH', '')}"
|
|
180
|
+
py_major = sys.version_info.major
|
|
181
|
+
py_minor = sys.version_info.minor
|
|
182
|
+
env["PYTHONPATH"] = str(
|
|
183
|
+
librelane_venv(flow_root)
|
|
184
|
+
/ "lib"
|
|
185
|
+
/ f"python{py_major}.{py_minor}"
|
|
186
|
+
/ "site-packages"
|
|
187
|
+
)
|
|
188
|
+
cmd = [
|
|
189
|
+
str(venv_bin / "python3"),
|
|
190
|
+
"-m",
|
|
191
|
+
"librelane",
|
|
192
|
+
"-m",
|
|
193
|
+
str(project_root),
|
|
194
|
+
"-m",
|
|
195
|
+
str(pdk_root),
|
|
196
|
+
]
|
|
197
|
+
try:
|
|
198
|
+
if not sys.stdin.isatty():
|
|
199
|
+
cmd.append("--docker-no-tty")
|
|
200
|
+
except Exception:
|
|
201
|
+
cmd.append("--docker-no-tty")
|
|
202
|
+
cmd.append("--dockerized")
|
|
203
|
+
cmd.extend(
|
|
204
|
+
[
|
|
205
|
+
"--manual-pdk",
|
|
206
|
+
"--pdk-root",
|
|
207
|
+
str(pdk_root),
|
|
208
|
+
"--pdk",
|
|
209
|
+
pdk,
|
|
210
|
+
"--ef-save-views-to",
|
|
211
|
+
str(project_root),
|
|
212
|
+
"--run-tag",
|
|
213
|
+
tag,
|
|
214
|
+
]
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
if overwrite and not from_step:
|
|
218
|
+
cmd.append("--overwrite")
|
|
219
|
+
if from_step:
|
|
220
|
+
cmd.extend(["--from", from_step])
|
|
221
|
+
if to_step:
|
|
222
|
+
cmd.extend(["--to", to_step])
|
|
223
|
+
cmd.append(str(config_file))
|
|
224
|
+
return cmd, env
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def new_run_tag() -> str:
|
|
228
|
+
return datetime.now().strftime("%y_%m_%d_%H_%M")
|
|
229
|
+
|
|
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
|
+
|
|
371
|
+
def find_synth_stat_json(flow_root: Path, macro: str, run_tag: str) -> Optional[Path]:
|
|
372
|
+
"""Locate Yosys stat.json written by a synth-only or full LibreLane run."""
|
|
373
|
+
run_dir = flow_root / macro / "runs" / run_tag
|
|
374
|
+
candidates = [
|
|
375
|
+
run_dir / "Yosys.Synthesis" / "reports" / "stat.json",
|
|
376
|
+
run_dir / "05-yosys-synthesis" / "reports" / "stat.json",
|
|
377
|
+
run_dir / "final" / "metrics.json",
|
|
378
|
+
]
|
|
379
|
+
if run_dir.is_dir():
|
|
380
|
+
for path in sorted(run_dir.rglob("stat.json")):
|
|
381
|
+
candidates.append(path)
|
|
382
|
+
for path in sorted(run_dir.rglob("metrics.json")):
|
|
383
|
+
candidates.append(path)
|
|
384
|
+
for path in candidates:
|
|
385
|
+
if path.is_file():
|
|
386
|
+
return path
|
|
387
|
+
return None
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def parse_synth_metrics(stat_path: Path) -> Dict[str, object]:
|
|
391
|
+
"""Parse Yosys stat.json or LibreLane metrics.json into gate/area fields.
|
|
392
|
+
|
|
393
|
+
Raises ValueError if the file cannot be parsed or has no cell count.
|
|
394
|
+
"""
|
|
395
|
+
raw = json.loads(stat_path.read_text())
|
|
396
|
+
if not isinstance(raw, dict):
|
|
397
|
+
raise ValueError(f"Synth metrics at {stat_path} must be a JSON object")
|
|
398
|
+
|
|
399
|
+
gate_count: Optional[int] = None
|
|
400
|
+
area_um2: Optional[float] = None
|
|
401
|
+
|
|
402
|
+
design = raw.get("design")
|
|
403
|
+
if isinstance(design, dict):
|
|
404
|
+
cells = design.get("num_cells")
|
|
405
|
+
if cells is not None:
|
|
406
|
+
gate_count = int(cells)
|
|
407
|
+
area = design.get("area")
|
|
408
|
+
if area not in (None, 0, 0.0, "0"):
|
|
409
|
+
area_um2 = float(area)
|
|
410
|
+
|
|
411
|
+
if gate_count is None and "design__instance__count" in raw:
|
|
412
|
+
gate_count = int(float(raw["design__instance__count"]))
|
|
413
|
+
if area_um2 is None and raw.get("design__instance__area") not in (None, "", 0, 0.0):
|
|
414
|
+
area_um2 = float(raw["design__instance__area"])
|
|
415
|
+
|
|
416
|
+
if gate_count is None:
|
|
417
|
+
raise ValueError(f"No cell count in synth metrics at {stat_path}")
|
|
418
|
+
|
|
419
|
+
area_mm2 = (area_um2 / 1_000_000.0) if area_um2 is not None else None
|
|
420
|
+
return {
|
|
421
|
+
"gate_count": gate_count,
|
|
422
|
+
"estimated_area_mm2": area_mm2,
|
|
423
|
+
"metrics_path": str(stat_path),
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def run_librelane(
|
|
428
|
+
cmd: Sequence[str],
|
|
429
|
+
*,
|
|
430
|
+
cwd: Path,
|
|
431
|
+
env: Dict[str, str],
|
|
432
|
+
) -> int:
|
|
433
|
+
process = subprocess.Popen(list(cmd), cwd=str(cwd), env=env)
|
|
434
|
+
return process.wait()
|