chipfoundry-cli 2.5.3__tar.gz → 2.6.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.3 → chipfoundry_cli-2.6.0}/PKG-INFO +1 -1
- {chipfoundry_cli-2.5.3 → chipfoundry_cli-2.6.0}/chipfoundry_cli/__init__.py +1 -1
- chipfoundry_cli-2.6.0/chipfoundry_cli/librelane_run.py +294 -0
- {chipfoundry_cli-2.5.3 → chipfoundry_cli-2.6.0}/chipfoundry_cli/main.py +70 -23
- chipfoundry_cli-2.6.0/chipfoundry_cli/preview.py +920 -0
- {chipfoundry_cli-2.5.3 → chipfoundry_cli-2.6.0}/pyproject.toml +1 -1
- {chipfoundry_cli-2.5.3 → chipfoundry_cli-2.6.0}/LICENSE +0 -0
- {chipfoundry_cli-2.5.3 → chipfoundry_cli-2.6.0}/README.md +0 -0
- {chipfoundry_cli-2.5.3 → chipfoundry_cli-2.6.0}/chipfoundry_cli/check_refs.py +0 -0
- {chipfoundry_cli-2.5.3 → chipfoundry_cli-2.6.0}/chipfoundry_cli/remote_precheck_git.py +0 -0
- {chipfoundry_cli-2.5.3 → chipfoundry_cli-2.6.0}/chipfoundry_cli/utils.py +0 -0
- {chipfoundry_cli-2.5.3 → chipfoundry_cli-2.6.0}/chipfoundry_cli/version_check.py +0 -0
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""ChipFoundry CLI package: Automate project submission to SFTP."""
|
|
2
|
-
__version__ = "2.
|
|
2
|
+
__version__ = "2.6.0"
|
|
@@ -0,0 +1,294 @@
|
|
|
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 find_synth_stat_json(flow_root: Path, macro: str, run_tag: str) -> Optional[Path]:
|
|
232
|
+
"""Locate Yosys stat.json written by a synth-only or full LibreLane run."""
|
|
233
|
+
run_dir = flow_root / macro / "runs" / run_tag
|
|
234
|
+
candidates = [
|
|
235
|
+
run_dir / "Yosys.Synthesis" / "reports" / "stat.json",
|
|
236
|
+
run_dir / "05-yosys-synthesis" / "reports" / "stat.json",
|
|
237
|
+
run_dir / "final" / "metrics.json",
|
|
238
|
+
]
|
|
239
|
+
if run_dir.is_dir():
|
|
240
|
+
for path in sorted(run_dir.rglob("stat.json")):
|
|
241
|
+
candidates.append(path)
|
|
242
|
+
for path in sorted(run_dir.rglob("metrics.json")):
|
|
243
|
+
candidates.append(path)
|
|
244
|
+
for path in candidates:
|
|
245
|
+
if path.is_file():
|
|
246
|
+
return path
|
|
247
|
+
return None
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def parse_synth_metrics(stat_path: Path) -> Dict[str, object]:
|
|
251
|
+
"""Parse Yosys stat.json or LibreLane metrics.json into gate/area fields.
|
|
252
|
+
|
|
253
|
+
Raises ValueError if the file cannot be parsed or has no cell count.
|
|
254
|
+
"""
|
|
255
|
+
raw = json.loads(stat_path.read_text())
|
|
256
|
+
if not isinstance(raw, dict):
|
|
257
|
+
raise ValueError(f"Synth metrics at {stat_path} must be a JSON object")
|
|
258
|
+
|
|
259
|
+
gate_count: Optional[int] = None
|
|
260
|
+
area_um2: Optional[float] = None
|
|
261
|
+
|
|
262
|
+
design = raw.get("design")
|
|
263
|
+
if isinstance(design, dict):
|
|
264
|
+
cells = design.get("num_cells")
|
|
265
|
+
if cells is not None:
|
|
266
|
+
gate_count = int(cells)
|
|
267
|
+
area = design.get("area")
|
|
268
|
+
if area not in (None, 0, 0.0, "0"):
|
|
269
|
+
area_um2 = float(area)
|
|
270
|
+
|
|
271
|
+
if gate_count is None and "design__instance__count" in raw:
|
|
272
|
+
gate_count = int(float(raw["design__instance__count"]))
|
|
273
|
+
if area_um2 is None and raw.get("design__instance__area") not in (None, "", 0, 0.0):
|
|
274
|
+
area_um2 = float(raw["design__instance__area"])
|
|
275
|
+
|
|
276
|
+
if gate_count is None:
|
|
277
|
+
raise ValueError(f"No cell count in synth metrics at {stat_path}")
|
|
278
|
+
|
|
279
|
+
area_mm2 = (area_um2 / 1_000_000.0) if area_um2 is not None else None
|
|
280
|
+
return {
|
|
281
|
+
"gate_count": gate_count,
|
|
282
|
+
"estimated_area_mm2": area_mm2,
|
|
283
|
+
"metrics_path": str(stat_path),
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def run_librelane(
|
|
288
|
+
cmd: Sequence[str],
|
|
289
|
+
*,
|
|
290
|
+
cwd: Path,
|
|
291
|
+
env: Dict[str, str],
|
|
292
|
+
) -> int:
|
|
293
|
+
process = subprocess.Popen(list(cmd), cwd=str(cwd), env=env)
|
|
294
|
+
return process.wait()
|
|
@@ -1735,6 +1735,39 @@ def _sha256_file(path: Path) -> str:
|
|
|
1735
1735
|
return h.hexdigest()
|
|
1736
1736
|
|
|
1737
1737
|
|
|
1738
|
+
def _ordered_sftp_uploads(upload_map: dict) -> list:
|
|
1739
|
+
"""Return payload files first and project.json last as the commit marker."""
|
|
1740
|
+
config_path = upload_map.get(".cf/project.json")
|
|
1741
|
+
if not config_path:
|
|
1742
|
+
raise ValueError("SFTP push requires .cf/project.json")
|
|
1743
|
+
payloads = [
|
|
1744
|
+
(rel_path, local_path)
|
|
1745
|
+
for rel_path, local_path in upload_map.items()
|
|
1746
|
+
if rel_path != ".cf/project.json" and local_path
|
|
1747
|
+
]
|
|
1748
|
+
payloads.append((".cf/project.json", config_path))
|
|
1749
|
+
return payloads
|
|
1750
|
+
|
|
1751
|
+
|
|
1752
|
+
def _prepared_wrapper_hash(project_json_path: str) -> str:
|
|
1753
|
+
with open(project_json_path, "r") as f:
|
|
1754
|
+
data = json.load(f)
|
|
1755
|
+
value = str((data.get("project") or {}).get("user_project_wrapper_hash") or "").strip()
|
|
1756
|
+
if not value:
|
|
1757
|
+
raise ValueError("project.json is missing project.user_project_wrapper_hash")
|
|
1758
|
+
return value
|
|
1759
|
+
|
|
1760
|
+
|
|
1761
|
+
def _assert_wrapper_hash_unchanged(wrapper_path: str, expected_hash: str) -> None:
|
|
1762
|
+
current_hash = _sha256_file(Path(wrapper_path))
|
|
1763
|
+
if current_hash != expected_hash:
|
|
1764
|
+
raise RuntimeError(
|
|
1765
|
+
"Wrapper GDS changed while it was being uploaded. "
|
|
1766
|
+
"project.json was not uploaded; stop any process modifying the GDS "
|
|
1767
|
+
"and run cf push --force-overwrite again."
|
|
1768
|
+
)
|
|
1769
|
+
|
|
1770
|
+
|
|
1738
1771
|
def _collect_push_candidates(project_root: Path) -> List[Tuple[str, Path, int, str]]:
|
|
1739
1772
|
"""Return [(rel_path, abs_path, size, kind)] for files the platform
|
|
1740
1773
|
accepts via --https.
|
|
@@ -2111,17 +2144,22 @@ def push(project_root, sftp_host, sftp_username, sftp_key, project_id, project_n
|
|
|
2111
2144
|
cf_dir = ensure_cf_directory(project_root)
|
|
2112
2145
|
|
|
2113
2146
|
# Find the GDS file path for hash calculation
|
|
2114
|
-
|
|
2115
|
-
for gds_key,
|
|
2147
|
+
wrapper_path = None
|
|
2148
|
+
for gds_key, local_path in collected.items():
|
|
2116
2149
|
if gds_key.startswith("gds/"):
|
|
2150
|
+
wrapper_path = local_path
|
|
2117
2151
|
break
|
|
2152
|
+
if not wrapper_path:
|
|
2153
|
+
console.print("[red]No wrapper GDS found in collected project files.[/red]")
|
|
2154
|
+
raise click.Abort()
|
|
2118
2155
|
|
|
2119
2156
|
project_json_path = update_or_create_project_json(
|
|
2120
2157
|
cf_dir=str(cf_dir),
|
|
2121
|
-
gds_path=
|
|
2158
|
+
gds_path=wrapper_path,
|
|
2122
2159
|
cli_overrides=cli_overrides,
|
|
2123
2160
|
existing_json_path=collected.get(".cf/project.json")
|
|
2124
2161
|
)
|
|
2162
|
+
prepared_wrapper_hash = _prepared_wrapper_hash(project_json_path)
|
|
2125
2163
|
|
|
2126
2164
|
# SFTP upload or dry-run
|
|
2127
2165
|
final_project_name = project_name or (
|
|
@@ -2135,16 +2173,14 @@ def push(project_root, sftp_host, sftp_username, sftp_key, project_id, project_n
|
|
|
2135
2173
|
upload_map["verilog/rtl/user_defines.v"] = collected.get("verilog/rtl/user_defines.v")
|
|
2136
2174
|
|
|
2137
2175
|
# Add the appropriate GDS file based on what was collected
|
|
2138
|
-
for gds_key,
|
|
2176
|
+
for gds_key, local_path in collected.items():
|
|
2139
2177
|
if gds_key.startswith("gds/"):
|
|
2140
|
-
upload_map[gds_key] =
|
|
2178
|
+
upload_map[gds_key] = local_path
|
|
2141
2179
|
|
|
2142
2180
|
if dry_run:
|
|
2143
2181
|
console.print("[bold]Files to upload:[/bold]")
|
|
2144
|
-
for rel_path, local_path in upload_map
|
|
2145
|
-
|
|
2146
|
-
remote_path = os.path.join(sftp_base, rel_path)
|
|
2147
|
-
console.print(f" {os.path.basename(local_path)} → {rel_path}")
|
|
2182
|
+
for rel_path, local_path in _ordered_sftp_uploads(upload_map):
|
|
2183
|
+
console.print(f" {os.path.basename(local_path)} → {rel_path}")
|
|
2148
2184
|
return
|
|
2149
2185
|
|
|
2150
2186
|
console.print(f"Connecting to {sftp_host}...")
|
|
@@ -2163,15 +2199,16 @@ def push(project_root, sftp_host, sftp_username, sftp_key, project_id, project_n
|
|
|
2163
2199
|
raise click.Abort()
|
|
2164
2200
|
|
|
2165
2201
|
try:
|
|
2166
|
-
for rel_path, local_path in upload_map
|
|
2167
|
-
if
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2202
|
+
for rel_path, local_path in _ordered_sftp_uploads(upload_map):
|
|
2203
|
+
if rel_path == ".cf/project.json":
|
|
2204
|
+
_assert_wrapper_hash_unchanged(wrapper_path, prepared_wrapper_hash)
|
|
2205
|
+
remote_path = os.path.join(sftp_base, rel_path)
|
|
2206
|
+
upload_with_progress(
|
|
2207
|
+
sftp,
|
|
2208
|
+
local_path=local_path,
|
|
2209
|
+
remote_path=remote_path,
|
|
2210
|
+
force_overwrite=force_overwrite
|
|
2211
|
+
)
|
|
2175
2212
|
console.print(f"[green]✓ Uploaded to {sftp_base}[/green]")
|
|
2176
2213
|
|
|
2177
2214
|
except Exception as e:
|
|
@@ -3764,8 +3801,12 @@ def _queue_and_maybe_poll_remote_job(
|
|
|
3764
3801
|
poll: bool,
|
|
3765
3802
|
wait_timeout: int,
|
|
3766
3803
|
label: str,
|
|
3767
|
-
)
|
|
3768
|
-
"""POST a remote platform job and optionally poll until terminal status.
|
|
3804
|
+
):
|
|
3805
|
+
"""POST a remote platform job and optionally poll until terminal status.
|
|
3806
|
+
|
|
3807
|
+
Returns the create-response job dict, or the last GET payload when polling
|
|
3808
|
+
completes. Returns None for dry-run.
|
|
3809
|
+
"""
|
|
3769
3810
|
import time
|
|
3770
3811
|
from urllib.parse import urlencode
|
|
3771
3812
|
|
|
@@ -3773,7 +3814,7 @@ def _queue_and_maybe_poll_remote_job(
|
|
|
3773
3814
|
|
|
3774
3815
|
if dry_run:
|
|
3775
3816
|
console.print(f"[cyan]Would POST[/cyan] {create_path}?" + urlencode(params))
|
|
3776
|
-
return
|
|
3817
|
+
return None
|
|
3777
3818
|
if poll and wait_timeout < 0:
|
|
3778
3819
|
console.print(
|
|
3779
3820
|
"[red]✗[/red] --wait-timeout must be >= 0 (0 means no limit while polling)."
|
|
@@ -3821,13 +3862,13 @@ def _queue_and_maybe_poll_remote_job(
|
|
|
3821
3862
|
console.print(f"[green]✓[/green] {label} completed")
|
|
3822
3863
|
if job.get("github_pr_url"):
|
|
3823
3864
|
console.print(f" Pull request: {job['github_pr_url']}")
|
|
3824
|
-
return
|
|
3865
|
+
return job
|
|
3825
3866
|
if not poll:
|
|
3826
3867
|
console.print(
|
|
3827
3868
|
f"[dim]Not waiting: use [bold]--remote --poll[/bold] to stream progress "
|
|
3828
3869
|
f"([bold]--wait-timeout 0[/bold] = no time limit while polling).[/dim]"
|
|
3829
3870
|
)
|
|
3830
|
-
return
|
|
3871
|
+
return job
|
|
3831
3872
|
deadline = None if wait_timeout == 0 else time.monotonic() + wait_timeout
|
|
3832
3873
|
if wait_timeout == 0:
|
|
3833
3874
|
console.print("[dim]Polling until the job completes (no timeout).[/dim]")
|
|
@@ -3891,6 +3932,7 @@ def _queue_and_maybe_poll_remote_job(
|
|
|
3891
3932
|
console.print(f"[green]✓[/green] {label} completed")
|
|
3892
3933
|
if github_pr_url:
|
|
3893
3934
|
console.print(f" Pull request: {github_pr_url}")
|
|
3935
|
+
return j2
|
|
3894
3936
|
elif terminal == "failed":
|
|
3895
3937
|
console.print(f"[red]✗[/red] {label} failed: {fail_message}")
|
|
3896
3938
|
raise SystemExit(1)
|
|
@@ -5745,5 +5787,10 @@ def whoami_cmd():
|
|
|
5745
5787
|
raise SystemExit(1)
|
|
5746
5788
|
|
|
5747
5789
|
|
|
5790
|
+
from chipfoundry_cli.preview import register_preview
|
|
5791
|
+
|
|
5792
|
+
register_preview(main)
|
|
5793
|
+
|
|
5794
|
+
|
|
5748
5795
|
if __name__ == "__main__":
|
|
5749
5796
|
main()
|