chipfoundry-cli 2.5.1__tar.gz → 2.5.4__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.1 → chipfoundry_cli-2.5.4}/PKG-INFO +1 -1
- {chipfoundry_cli-2.5.1 → chipfoundry_cli-2.5.4}/chipfoundry_cli/__init__.py +1 -1
- {chipfoundry_cli-2.5.1 → chipfoundry_cli-2.5.4}/chipfoundry_cli/main.py +418 -29
- {chipfoundry_cli-2.5.1 → chipfoundry_cli-2.5.4}/chipfoundry_cli/remote_precheck_git.py +41 -0
- {chipfoundry_cli-2.5.1 → chipfoundry_cli-2.5.4}/pyproject.toml +1 -1
- {chipfoundry_cli-2.5.1 → chipfoundry_cli-2.5.4}/LICENSE +0 -0
- {chipfoundry_cli-2.5.1 → chipfoundry_cli-2.5.4}/README.md +0 -0
- {chipfoundry_cli-2.5.1 → chipfoundry_cli-2.5.4}/chipfoundry_cli/check_refs.py +0 -0
- {chipfoundry_cli-2.5.1 → chipfoundry_cli-2.5.4}/chipfoundry_cli/utils.py +0 -0
- {chipfoundry_cli-2.5.1 → chipfoundry_cli-2.5.4}/chipfoundry_cli/version_check.py +0 -0
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""ChipFoundry CLI package: Automate project submission to SFTP."""
|
|
2
|
-
__version__ = "2.4
|
|
2
|
+
__version__ = "2.5.4"
|
|
@@ -3,7 +3,11 @@ import getpass
|
|
|
3
3
|
import hashlib
|
|
4
4
|
from typing import Optional, List, Tuple
|
|
5
5
|
from chipfoundry_cli.check_refs import PRECHECK_CHECKS
|
|
6
|
-
from chipfoundry_cli.remote_precheck_git import
|
|
6
|
+
from chipfoundry_cli.remote_precheck_git import (
|
|
7
|
+
RemotePrecheckGitError,
|
|
8
|
+
verify_remote_job_repo,
|
|
9
|
+
verify_remote_precheck_repo,
|
|
10
|
+
)
|
|
7
11
|
from chipfoundry_cli.version_check import maybe_warn_outdated
|
|
8
12
|
from chipfoundry_cli.utils import (
|
|
9
13
|
collect_project_files, ensure_cf_directory, update_or_create_project_json,
|
|
@@ -1731,6 +1735,39 @@ def _sha256_file(path: Path) -> str:
|
|
|
1731
1735
|
return h.hexdigest()
|
|
1732
1736
|
|
|
1733
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
|
+
|
|
1734
1771
|
def _collect_push_candidates(project_root: Path) -> List[Tuple[str, Path, int, str]]:
|
|
1735
1772
|
"""Return [(rel_path, abs_path, size, kind)] for files the platform
|
|
1736
1773
|
accepts via --https.
|
|
@@ -2107,17 +2144,22 @@ def push(project_root, sftp_host, sftp_username, sftp_key, project_id, project_n
|
|
|
2107
2144
|
cf_dir = ensure_cf_directory(project_root)
|
|
2108
2145
|
|
|
2109
2146
|
# Find the GDS file path for hash calculation
|
|
2110
|
-
|
|
2111
|
-
for gds_key,
|
|
2147
|
+
wrapper_path = None
|
|
2148
|
+
for gds_key, local_path in collected.items():
|
|
2112
2149
|
if gds_key.startswith("gds/"):
|
|
2150
|
+
wrapper_path = local_path
|
|
2113
2151
|
break
|
|
2152
|
+
if not wrapper_path:
|
|
2153
|
+
console.print("[red]No wrapper GDS found in collected project files.[/red]")
|
|
2154
|
+
raise click.Abort()
|
|
2114
2155
|
|
|
2115
2156
|
project_json_path = update_or_create_project_json(
|
|
2116
2157
|
cf_dir=str(cf_dir),
|
|
2117
|
-
gds_path=
|
|
2158
|
+
gds_path=wrapper_path,
|
|
2118
2159
|
cli_overrides=cli_overrides,
|
|
2119
2160
|
existing_json_path=collected.get(".cf/project.json")
|
|
2120
2161
|
)
|
|
2162
|
+
prepared_wrapper_hash = _prepared_wrapper_hash(project_json_path)
|
|
2121
2163
|
|
|
2122
2164
|
# SFTP upload or dry-run
|
|
2123
2165
|
final_project_name = project_name or (
|
|
@@ -2131,16 +2173,14 @@ def push(project_root, sftp_host, sftp_username, sftp_key, project_id, project_n
|
|
|
2131
2173
|
upload_map["verilog/rtl/user_defines.v"] = collected.get("verilog/rtl/user_defines.v")
|
|
2132
2174
|
|
|
2133
2175
|
# Add the appropriate GDS file based on what was collected
|
|
2134
|
-
for gds_key,
|
|
2176
|
+
for gds_key, local_path in collected.items():
|
|
2135
2177
|
if gds_key.startswith("gds/"):
|
|
2136
|
-
upload_map[gds_key] =
|
|
2178
|
+
upload_map[gds_key] = local_path
|
|
2137
2179
|
|
|
2138
2180
|
if dry_run:
|
|
2139
2181
|
console.print("[bold]Files to upload:[/bold]")
|
|
2140
|
-
for rel_path, local_path in upload_map
|
|
2141
|
-
|
|
2142
|
-
remote_path = os.path.join(sftp_base, rel_path)
|
|
2143
|
-
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}")
|
|
2144
2184
|
return
|
|
2145
2185
|
|
|
2146
2186
|
console.print(f"Connecting to {sftp_host}...")
|
|
@@ -2159,15 +2199,16 @@ def push(project_root, sftp_host, sftp_username, sftp_key, project_id, project_n
|
|
|
2159
2199
|
raise click.Abort()
|
|
2160
2200
|
|
|
2161
2201
|
try:
|
|
2162
|
-
for rel_path, local_path in upload_map
|
|
2163
|
-
if
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
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
|
+
)
|
|
2171
2212
|
console.print(f"[green]✓ Uploaded to {sftp_base}[/green]")
|
|
2172
2213
|
|
|
2173
2214
|
except Exception as e:
|
|
@@ -3587,10 +3628,12 @@ def setup(project_root, repo_owner, repo_name, branch, pdk, caravel_lite,
|
|
|
3587
3628
|
check=True,
|
|
3588
3629
|
capture_output=True
|
|
3589
3630
|
)
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
|
|
3631
|
+
|
|
3632
|
+
venv_python = str(venv_cocotb / 'bin' / 'python3')
|
|
3633
|
+
|
|
3634
|
+
# Install (or reinstall) when the package is missing or --overwrite was used.
|
|
3635
|
+
# The venv may already exist from a previous partial failure.
|
|
3636
|
+
if not is_installed or overwrite:
|
|
3594
3637
|
console.print("[cyan]Installing caravel-cocotb from source (chipfoundry/caravel-sim-infrastructure)...[/cyan]")
|
|
3595
3638
|
subprocess.run(
|
|
3596
3639
|
[venv_python, '-m', 'pip', 'install', '--upgrade', '--no-cache-dir', 'pip'],
|
|
@@ -3695,7 +3738,208 @@ def setup(project_root, repo_owner, repo_name, branch, pdk, caravel_lite,
|
|
|
3695
3738
|
console.print("[bold green]Installation complete![/bold green]")
|
|
3696
3739
|
else:
|
|
3697
3740
|
console.print("[bold green]Setup complete![/bold green]")
|
|
3698
|
-
|
|
3741
|
+
|
|
3742
|
+
|
|
3743
|
+
def _is_nix_toolchain_noise_line(line: str) -> bool:
|
|
3744
|
+
"""Match Nix fetch/build chatter posted by the PNR runner."""
|
|
3745
|
+
s = line.strip()
|
|
3746
|
+
if not s:
|
|
3747
|
+
return True
|
|
3748
|
+
bare = s[len("[stderr]") :].lstrip() if s.startswith("[stderr]") else s
|
|
3749
|
+
low = bare.lower()
|
|
3750
|
+
if low.startswith("copying path"):
|
|
3751
|
+
return True
|
|
3752
|
+
if low.startswith("building '/nix/store/") or low.startswith('building "/nix/store/'):
|
|
3753
|
+
return True
|
|
3754
|
+
if low.startswith("downloading '") or low.startswith('downloading "'):
|
|
3755
|
+
return True
|
|
3756
|
+
if low.startswith("unpacking 'github:") or low.startswith('unpacking "github:'):
|
|
3757
|
+
return True
|
|
3758
|
+
if "paths will be fetched" in low or "paths will be copied" in low:
|
|
3759
|
+
return True
|
|
3760
|
+
if low.startswith("copying ") and " path" in low:
|
|
3761
|
+
return True
|
|
3762
|
+
if low.startswith("waiting for lock") or low.startswith("waiting for locks"):
|
|
3763
|
+
return True
|
|
3764
|
+
if bare.startswith("/nix/store/") or bare.startswith(" /nix/store/"):
|
|
3765
|
+
return True
|
|
3766
|
+
return False
|
|
3767
|
+
|
|
3768
|
+
|
|
3769
|
+
def _print_remote_progress_message(msg: object, *, style: str = "dim") -> None:
|
|
3770
|
+
"""Print worker progress, collapsing Nix toolchain noise into a one-liner."""
|
|
3771
|
+
text = str(msg)
|
|
3772
|
+
lines = text.splitlines()
|
|
3773
|
+
kept: List[str] = []
|
|
3774
|
+
noise = 0
|
|
3775
|
+
for line in lines:
|
|
3776
|
+
if _is_nix_toolchain_noise_line(line):
|
|
3777
|
+
noise += 1
|
|
3778
|
+
else:
|
|
3779
|
+
kept.append(line)
|
|
3780
|
+
if noise and not kept:
|
|
3781
|
+
console.print(
|
|
3782
|
+
Text(
|
|
3783
|
+
f"Nix toolchain resolving… ({noise} build/fetch lines suppressed)",
|
|
3784
|
+
style="dim",
|
|
3785
|
+
)
|
|
3786
|
+
)
|
|
3787
|
+
return
|
|
3788
|
+
if noise:
|
|
3789
|
+
kept.append(f"(also suppressed {noise} Nix build/fetch lines)")
|
|
3790
|
+
out = "\n".join(kept).rstrip()
|
|
3791
|
+
if out:
|
|
3792
|
+
console.print(Text(out, style=style))
|
|
3793
|
+
|
|
3794
|
+
|
|
3795
|
+
def _queue_and_maybe_poll_remote_job(
|
|
3796
|
+
*,
|
|
3797
|
+
create_path: str,
|
|
3798
|
+
job_get_path_template: str,
|
|
3799
|
+
params: list,
|
|
3800
|
+
dry_run: bool,
|
|
3801
|
+
poll: bool,
|
|
3802
|
+
wait_timeout: int,
|
|
3803
|
+
label: str,
|
|
3804
|
+
) -> None:
|
|
3805
|
+
"""POST a remote platform job and optionally poll until terminal status."""
|
|
3806
|
+
import time
|
|
3807
|
+
from urllib.parse import urlencode
|
|
3808
|
+
|
|
3809
|
+
import httpx as httpx_remote
|
|
3810
|
+
|
|
3811
|
+
if dry_run:
|
|
3812
|
+
console.print(f"[cyan]Would POST[/cyan] {create_path}?" + urlencode(params))
|
|
3813
|
+
return
|
|
3814
|
+
if poll and wait_timeout < 0:
|
|
3815
|
+
console.print(
|
|
3816
|
+
"[red]✗[/red] --wait-timeout must be >= 0 (0 means no limit while polling)."
|
|
3817
|
+
)
|
|
3818
|
+
raise SystemExit(1)
|
|
3819
|
+
config = load_user_config()
|
|
3820
|
+
api_key = config.get('api_key')
|
|
3821
|
+
if not api_key:
|
|
3822
|
+
console.print("[yellow]Not logged in.[/yellow] Run [bold]cf login[/bold] first.")
|
|
3823
|
+
raise SystemExit(1)
|
|
3824
|
+
api_url = _get_api_url()
|
|
3825
|
+
client = httpx_remote.Client(
|
|
3826
|
+
base_url=f"{api_url}/api/v1",
|
|
3827
|
+
headers={
|
|
3828
|
+
'Authorization': f'Bearer {api_key}',
|
|
3829
|
+
'User-Agent': _cf_user_agent(),
|
|
3830
|
+
},
|
|
3831
|
+
timeout=120.0,
|
|
3832
|
+
)
|
|
3833
|
+
try:
|
|
3834
|
+
resp = client.post(create_path, params=params)
|
|
3835
|
+
if resp.status_code == 401:
|
|
3836
|
+
console.print("[red]✗[/red] API key is invalid or expired. Run [bold]cf login[/bold].")
|
|
3837
|
+
raise SystemExit(1)
|
|
3838
|
+
if not resp.is_success:
|
|
3839
|
+
try:
|
|
3840
|
+
detail = resp.json().get("detail", resp.text)
|
|
3841
|
+
except Exception:
|
|
3842
|
+
detail = resp.text
|
|
3843
|
+
console.print(f"[red]✗[/red] {detail}")
|
|
3844
|
+
raise SystemExit(1)
|
|
3845
|
+
job = resp.json()
|
|
3846
|
+
jid = job["id"]
|
|
3847
|
+
st0 = job.get("status") or "unknown"
|
|
3848
|
+
if st0 == "failed":
|
|
3849
|
+
console.print(f"[cyan]{label}[/cyan] job_id={jid} status={st0}")
|
|
3850
|
+
elif st0 == "running":
|
|
3851
|
+
console.print(f"[cyan]{label} started[/cyan] job_id={jid} status={st0}")
|
|
3852
|
+
else:
|
|
3853
|
+
console.print(f"[cyan]Queued {label.lower()}[/cyan] job_id={jid} status={st0}")
|
|
3854
|
+
if job.get("status") == "failed" and job.get("error_message"):
|
|
3855
|
+
console.print(f"[red]✗[/red] {job['error_message']}")
|
|
3856
|
+
raise SystemExit(1)
|
|
3857
|
+
if job.get("status") == "completed":
|
|
3858
|
+
console.print(f"[green]✓[/green] {label} completed")
|
|
3859
|
+
if job.get("github_pr_url"):
|
|
3860
|
+
console.print(f" Pull request: {job['github_pr_url']}")
|
|
3861
|
+
return
|
|
3862
|
+
if not poll:
|
|
3863
|
+
console.print(
|
|
3864
|
+
f"[dim]Not waiting: use [bold]--remote --poll[/bold] to stream progress "
|
|
3865
|
+
f"([bold]--wait-timeout 0[/bold] = no time limit while polling).[/dim]"
|
|
3866
|
+
)
|
|
3867
|
+
return
|
|
3868
|
+
deadline = None if wait_timeout == 0 else time.monotonic() + wait_timeout
|
|
3869
|
+
if wait_timeout == 0:
|
|
3870
|
+
console.print("[dim]Polling until the job completes (no timeout).[/dim]")
|
|
3871
|
+
else:
|
|
3872
|
+
console.print(
|
|
3873
|
+
f"[dim]Polling every 5s; stops after {wait_timeout}s if still queued or running. "
|
|
3874
|
+
f"Use [bold]--wait-timeout 0[/bold] for no limit.[/dim]"
|
|
3875
|
+
)
|
|
3876
|
+
last_status_seen = st0
|
|
3877
|
+
terminal = None
|
|
3878
|
+
github_pr_url = None
|
|
3879
|
+
fail_message = None
|
|
3880
|
+
progress_emitted = 0
|
|
3881
|
+
get_path = job_get_path_template.format(jid=jid)
|
|
3882
|
+
console.print("[dim]Worker log batches appear below as the platform receives them (5s poll).[/dim]")
|
|
3883
|
+
while True:
|
|
3884
|
+
if deadline is not None and time.monotonic() > deadline:
|
|
3885
|
+
console.print(
|
|
3886
|
+
f"[yellow]⚠[/yellow] Timed out waiting for {label.lower()} (job still queued or running)."
|
|
3887
|
+
)
|
|
3888
|
+
console.print(
|
|
3889
|
+
f"[dim]job_id={jid} — open the project in the portal or run [bold]cf status[/bold].[/dim]"
|
|
3890
|
+
)
|
|
3891
|
+
console.print(
|
|
3892
|
+
"[dim]Cancel a stuck run in the portal, or retry with e.g. "
|
|
3893
|
+
"[bold]--remote --poll --wait-timeout 14400[/bold].[/dim]"
|
|
3894
|
+
)
|
|
3895
|
+
raise SystemExit(1)
|
|
3896
|
+
time.sleep(5)
|
|
3897
|
+
r2 = client.get(get_path)
|
|
3898
|
+
if r2.status_code == 401:
|
|
3899
|
+
console.print("[red]✗[/red] API key is invalid or expired.")
|
|
3900
|
+
raise SystemExit(1)
|
|
3901
|
+
r2.raise_for_status()
|
|
3902
|
+
j2 = r2.json()
|
|
3903
|
+
st = j2.get("status")
|
|
3904
|
+
prog = j2.get("progress")
|
|
3905
|
+
if isinstance(prog, list) and len(prog) > progress_emitted:
|
|
3906
|
+
for row in prog[progress_emitted:]:
|
|
3907
|
+
if not isinstance(row, dict):
|
|
3908
|
+
continue
|
|
3909
|
+
msg = row.get("message")
|
|
3910
|
+
if msg:
|
|
3911
|
+
_print_remote_progress_message(msg)
|
|
3912
|
+
progress_emitted = len(prog)
|
|
3913
|
+
if st == "completed":
|
|
3914
|
+
terminal = "completed"
|
|
3915
|
+
github_pr_url = j2.get("github_pr_url")
|
|
3916
|
+
break
|
|
3917
|
+
if st == "failed":
|
|
3918
|
+
terminal = "failed"
|
|
3919
|
+
fail_message = j2.get("error_message") or "unknown error"
|
|
3920
|
+
break
|
|
3921
|
+
if st != last_status_seen:
|
|
3922
|
+
console.print(
|
|
3923
|
+
f"[dim]… job status[/dim] [cyan]{st or 'unknown'}[/cyan]"
|
|
3924
|
+
)
|
|
3925
|
+
last_status_seen = st
|
|
3926
|
+
|
|
3927
|
+
if terminal == "completed":
|
|
3928
|
+
console.print(f"[green]✓[/green] {label} completed")
|
|
3929
|
+
if github_pr_url:
|
|
3930
|
+
console.print(f" Pull request: {github_pr_url}")
|
|
3931
|
+
elif terminal == "failed":
|
|
3932
|
+
console.print(f"[red]✗[/red] {label} failed: {fail_message}")
|
|
3933
|
+
raise SystemExit(1)
|
|
3934
|
+
except SystemExit:
|
|
3935
|
+
raise
|
|
3936
|
+
except Exception as e:
|
|
3937
|
+
console.print(f"[red]✗[/red] {label} request failed: {e}")
|
|
3938
|
+
raise SystemExit(1)
|
|
3939
|
+
finally:
|
|
3940
|
+
client.close()
|
|
3941
|
+
|
|
3942
|
+
|
|
3699
3943
|
@main.command(
|
|
3700
3944
|
'harden',
|
|
3701
3945
|
cls=CategorizedCommand,
|
|
@@ -3704,6 +3948,7 @@ def setup(project_root, repo_owner, repo_name, branch, pdk, caravel_lite,
|
|
|
3704
3948
|
("Run Controls", ["tag", "from_step"]),
|
|
3705
3949
|
("GUI Modes", ["open_in_openroad", "open_in_klayout"]),
|
|
3706
3950
|
("Execution Backend", ["pdk", "use_nix", "use_docker"]),
|
|
3951
|
+
("Remote", ["dry_run", "remote", "poll", "git_ref", "wait_timeout"]),
|
|
3707
3952
|
],
|
|
3708
3953
|
)
|
|
3709
3954
|
@click.argument('macro', required=False)
|
|
@@ -3717,8 +3962,46 @@ def setup(project_root, repo_owner, repo_name, branch, pdk, caravel_lite,
|
|
|
3717
3962
|
@click.option('--pdk', help='PDK to use (defaults to sky130A)')
|
|
3718
3963
|
@click.option('--use-nix', is_flag=True, help='Force use of Nix (fails if Nix not available)')
|
|
3719
3964
|
@click.option('--use-docker', is_flag=True, help='Force use of Docker (fails if Docker not available)')
|
|
3720
|
-
|
|
3721
|
-
|
|
3965
|
+
@click.option('--dry-run', is_flag=True, help='Show the configuration without running')
|
|
3966
|
+
@click.option('--remote', is_flag=True, help='Queue PNR on the ChipFoundry platform (requires cf login + linked project)')
|
|
3967
|
+
@click.option(
|
|
3968
|
+
'--poll',
|
|
3969
|
+
is_flag=True,
|
|
3970
|
+
help='With --remote: poll until the job finishes and print progress (5s interval).',
|
|
3971
|
+
)
|
|
3972
|
+
@click.option('--git-ref', default='main', show_default=True, help='Git branch or tag for remote PNR')
|
|
3973
|
+
@click.option(
|
|
3974
|
+
'--wait-timeout',
|
|
3975
|
+
type=int,
|
|
3976
|
+
default=7200,
|
|
3977
|
+
show_default=True,
|
|
3978
|
+
help='With --remote --poll: max seconds to wait (0 = no limit). Ignored without --poll.',
|
|
3979
|
+
)
|
|
3980
|
+
def harden(
|
|
3981
|
+
macro,
|
|
3982
|
+
project_root,
|
|
3983
|
+
list_designs,
|
|
3984
|
+
list_from_steps,
|
|
3985
|
+
tag,
|
|
3986
|
+
from_step,
|
|
3987
|
+
open_in_openroad,
|
|
3988
|
+
open_in_klayout,
|
|
3989
|
+
pdk,
|
|
3990
|
+
use_nix,
|
|
3991
|
+
use_docker,
|
|
3992
|
+
dry_run,
|
|
3993
|
+
remote,
|
|
3994
|
+
poll,
|
|
3995
|
+
git_ref,
|
|
3996
|
+
wait_timeout,
|
|
3997
|
+
):
|
|
3998
|
+
"""Harden a macro using LibreLane (OpenLane 2).
|
|
3999
|
+
|
|
4000
|
+
Examples:
|
|
4001
|
+
cf harden user_proj_example
|
|
4002
|
+
cf harden --list
|
|
4003
|
+
cf harden user_proj_example --remote --poll
|
|
4004
|
+
"""
|
|
3722
4005
|
from datetime import datetime
|
|
3723
4006
|
|
|
3724
4007
|
# If .cf/project.json exists in cwd, use it as default project_root
|
|
@@ -3729,6 +4012,49 @@ def harden(macro, project_root, list_designs, list_from_steps, tag, from_step, o
|
|
|
3729
4012
|
project_root = os.getcwd()
|
|
3730
4013
|
|
|
3731
4014
|
project_root_path = Path(project_root)
|
|
4015
|
+
|
|
4016
|
+
if poll and not remote:
|
|
4017
|
+
console.print("[red]✗[/red] --poll requires --remote.")
|
|
4018
|
+
raise SystemExit(1)
|
|
4019
|
+
|
|
4020
|
+
if remote:
|
|
4021
|
+
if list_designs or not macro:
|
|
4022
|
+
console.print("[red]✗[/red] --remote requires a macro name (cannot use --list).")
|
|
4023
|
+
raise SystemExit(1)
|
|
4024
|
+
if list_from_steps or from_step or open_in_openroad or open_in_klayout:
|
|
4025
|
+
console.print(
|
|
4026
|
+
"[red]✗[/red] --remote cannot be combined with --from, --list-from-steps, or GUI modes."
|
|
4027
|
+
)
|
|
4028
|
+
raise SystemExit(1)
|
|
4029
|
+
if not check_project_initialized(project_root_path, 'harden', dry_run=dry_run, allow_graceful=True):
|
|
4030
|
+
console.print(f"[red]✗[/red] Project not initialized. Please run 'cf init' first.")
|
|
4031
|
+
return
|
|
4032
|
+
platform_id = _load_project_platform_id(str(project_root_path))
|
|
4033
|
+
if not platform_id:
|
|
4034
|
+
console.print(
|
|
4035
|
+
"[red]✗[/red] Link this repo to a platform project (set platform_project_id via [bold]cf link[/bold])."
|
|
4036
|
+
)
|
|
4037
|
+
raise SystemExit(1)
|
|
4038
|
+
try:
|
|
4039
|
+
verify_remote_job_repo(project_root_path, git_ref)
|
|
4040
|
+
except RemotePrecheckGitError as e:
|
|
4041
|
+
console.print(f"[red]✗[/red] {e}")
|
|
4042
|
+
raise SystemExit(1)
|
|
4043
|
+
remote_params = [("macro", macro), ("git_ref", git_ref)]
|
|
4044
|
+
if pdk:
|
|
4045
|
+
remote_params.append(("pdk", pdk))
|
|
4046
|
+
if tag:
|
|
4047
|
+
remote_params.append(("run_tag", tag))
|
|
4048
|
+
_queue_and_maybe_poll_remote_job(
|
|
4049
|
+
create_path=f"/projects/{platform_id}/pnr-jobs",
|
|
4050
|
+
job_get_path_template=f"/projects/{platform_id}/pnr-jobs/{{jid}}",
|
|
4051
|
+
params=remote_params,
|
|
4052
|
+
dry_run=dry_run,
|
|
4053
|
+
poll=poll,
|
|
4054
|
+
wait_timeout=wait_timeout,
|
|
4055
|
+
label="Remote PNR",
|
|
4056
|
+
)
|
|
4057
|
+
return
|
|
3732
4058
|
|
|
3733
4059
|
# Check if project is initialized (skip check for --list or when no macro specified, allow graceful return)
|
|
3734
4060
|
if not list_designs and macro:
|
|
@@ -4491,9 +4817,9 @@ def precheck(project_root, skip_checks, magic_drc, checks, list_checks, dry_run,
|
|
|
4491
4817
|
isinstance(det, dict)
|
|
4492
4818
|
and det.get("event") == "check_done"
|
|
4493
4819
|
):
|
|
4494
|
-
|
|
4820
|
+
_print_remote_progress_message(msg, style="bold")
|
|
4495
4821
|
else:
|
|
4496
|
-
|
|
4822
|
+
_print_remote_progress_message(msg)
|
|
4497
4823
|
progress_emitted = len(prog)
|
|
4498
4824
|
if st == "completed":
|
|
4499
4825
|
terminal = "completed"
|
|
@@ -4668,7 +4994,21 @@ def precheck(project_root, skip_checks, magic_drc, checks, list_checks, dry_run,
|
|
|
4668
4994
|
@click.option('--all', 'run_all', is_flag=True, help='Run all tests')
|
|
4669
4995
|
@click.option('--tag', help='Test list tag/yaml file (e.g., all_tests or user_proj_tests)')
|
|
4670
4996
|
@click.option('--dry-run', is_flag=True, help='Show the configuration without running')
|
|
4671
|
-
|
|
4997
|
+
@click.option('--remote', is_flag=True, help='Queue simulation on the ChipFoundry platform (requires cf login + linked project)')
|
|
4998
|
+
@click.option(
|
|
4999
|
+
'--poll',
|
|
5000
|
+
is_flag=True,
|
|
5001
|
+
help='With --remote: poll until the job finishes and print progress (5s interval).',
|
|
5002
|
+
)
|
|
5003
|
+
@click.option('--git-ref', default='main', show_default=True, help='Git branch or tag for remote simulation')
|
|
5004
|
+
@click.option(
|
|
5005
|
+
'--wait-timeout',
|
|
5006
|
+
type=int,
|
|
5007
|
+
default=7200,
|
|
5008
|
+
show_default=True,
|
|
5009
|
+
help='With --remote --poll: max seconds to wait (0 = no limit). Ignored without --poll.',
|
|
5010
|
+
)
|
|
5011
|
+
def verify(test, project_root, sim, list_tests, run_all, tag, dry_run, remote, poll, git_ref, wait_timeout):
|
|
4672
5012
|
"""Run cocotb verification tests.
|
|
4673
5013
|
|
|
4674
5014
|
Examples:
|
|
@@ -4677,6 +5017,8 @@ def verify(test, project_root, sim, list_tests, run_all, tag, dry_run):
|
|
|
4677
5017
|
cf verify counter_la --sim gl # Run gate-level simulation
|
|
4678
5018
|
cf verify --all # Run all tests
|
|
4679
5019
|
cf verify --tag all_tests # Run tests from a yaml list
|
|
5020
|
+
cf verify counter_la --remote
|
|
5021
|
+
cf verify --all --remote --poll
|
|
4680
5022
|
"""
|
|
4681
5023
|
# If .cf/project.json exists in cwd, use it as default project_root
|
|
4682
5024
|
cwd_root, _ = get_project_json_from_cwd()
|
|
@@ -4686,6 +5028,53 @@ def verify(test, project_root, sim, list_tests, run_all, tag, dry_run):
|
|
|
4686
5028
|
project_root = os.getcwd()
|
|
4687
5029
|
|
|
4688
5030
|
project_root_path = Path(project_root)
|
|
5031
|
+
|
|
5032
|
+
if poll and not remote:
|
|
5033
|
+
console.print("[red]✗[/red] --poll requires --remote.")
|
|
5034
|
+
raise SystemExit(1)
|
|
5035
|
+
|
|
5036
|
+
if remote:
|
|
5037
|
+
if list_tests:
|
|
5038
|
+
console.print("[red]✗[/red] --remote cannot be combined with --list.")
|
|
5039
|
+
raise SystemExit(1)
|
|
5040
|
+
if not check_project_initialized(project_root_path, 'verify', dry_run=dry_run, allow_graceful=True):
|
|
5041
|
+
console.print(f"[red]✗[/red] Project not initialized. Please run 'cf init' first.")
|
|
5042
|
+
return
|
|
5043
|
+
if not test and not run_all and not tag:
|
|
5044
|
+
console.print("[red]Error: Specify a test name, use --all, or --tag <test_list>[/red]")
|
|
5045
|
+
raise SystemExit(1)
|
|
5046
|
+
mode_count = int(bool(test)) + int(bool(run_all)) + int(bool(tag))
|
|
5047
|
+
if mode_count != 1:
|
|
5048
|
+
console.print("[red]Error: Use exactly one of: test name, --all, or --tag[/red]")
|
|
5049
|
+
raise SystemExit(1)
|
|
5050
|
+
platform_id = _load_project_platform_id(str(project_root_path))
|
|
5051
|
+
if not platform_id:
|
|
5052
|
+
console.print(
|
|
5053
|
+
"[red]✗[/red] Link this repo to a platform project (set platform_project_id via [bold]cf link[/bold])."
|
|
5054
|
+
)
|
|
5055
|
+
raise SystemExit(1)
|
|
5056
|
+
try:
|
|
5057
|
+
verify_remote_job_repo(project_root_path, git_ref)
|
|
5058
|
+
except RemotePrecheckGitError as e:
|
|
5059
|
+
console.print(f"[red]✗[/red] {e}")
|
|
5060
|
+
raise SystemExit(1)
|
|
5061
|
+
remote_params = [("git_ref", git_ref), ("sim_type", (sim or "rtl").lower())]
|
|
5062
|
+
if run_all:
|
|
5063
|
+
remote_params.append(("run_all", "true"))
|
|
5064
|
+
elif tag:
|
|
5065
|
+
remote_params.append(("test_list_tag", tag))
|
|
5066
|
+
else:
|
|
5067
|
+
remote_params.append(("test", test))
|
|
5068
|
+
_queue_and_maybe_poll_remote_job(
|
|
5069
|
+
create_path=f"/projects/{platform_id}/simulation-jobs",
|
|
5070
|
+
job_get_path_template=f"/projects/{platform_id}/simulation-jobs/{{jid}}",
|
|
5071
|
+
params=remote_params,
|
|
5072
|
+
dry_run=dry_run,
|
|
5073
|
+
poll=poll,
|
|
5074
|
+
wait_timeout=wait_timeout,
|
|
5075
|
+
label="Remote simulation",
|
|
5076
|
+
)
|
|
5077
|
+
return
|
|
4689
5078
|
|
|
4690
5079
|
# Check if project is initialized (skip check if just listing tests, allow graceful return)
|
|
4691
5080
|
if not list_tests:
|
|
@@ -228,6 +228,47 @@ def verify_remote_precheck_repo(
|
|
|
228
228
|
)
|
|
229
229
|
|
|
230
230
|
|
|
231
|
+
def verify_remote_job_repo(project_root: Path, git_ref: str) -> None:
|
|
232
|
+
"""
|
|
233
|
+
Thin git consistency check for remote harden / verify.
|
|
234
|
+
|
|
235
|
+
Ensures origin/{git_ref} tip matches HEAD. Does not require wrapper GDS or
|
|
236
|
+
other precheck layout inputs (those may not exist yet when hardening).
|
|
237
|
+
"""
|
|
238
|
+
repo = project_root.resolve()
|
|
239
|
+
git_marker = repo / ".git"
|
|
240
|
+
if not (git_marker.is_dir() or git_marker.is_file()):
|
|
241
|
+
raise RemotePrecheckGitError(
|
|
242
|
+
"Remote jobs require a git checkout with .git "
|
|
243
|
+
"(clone your GitHub repo, not a plain folder copy)."
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
remote_sha = _resolve_origin_tip_sha(repo, git_ref)
|
|
247
|
+
head_sha = _local_head_sha(repo)
|
|
248
|
+
if head_sha != remote_sha:
|
|
249
|
+
raise RemotePrecheckGitError(
|
|
250
|
+
f"Local HEAD ({head_sha[:7]}) must match origin {git_ref!r} ({remote_sha[:7]}). "
|
|
251
|
+
f"git checkout {git_ref} && git pull, or push your commits, then retry."
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
# If .cf/project.json is tracked, require it clean so the remote clone matches.
|
|
255
|
+
if _path_tracked_in_git(repo, CF_PROJECT_JSON_REL):
|
|
256
|
+
dirty = _porcelain_paths(repo)
|
|
257
|
+
for entry in dirty:
|
|
258
|
+
path = entry[2:] if entry.startswith("??") else entry
|
|
259
|
+
if path == CF_PROJECT_JSON_REL:
|
|
260
|
+
raise RemotePrecheckGitError(
|
|
261
|
+
f"{CF_PROJECT_JSON_REL!r} has uncommitted changes. "
|
|
262
|
+
"Commit or stash before queueing a remote job."
|
|
263
|
+
)
|
|
264
|
+
r = _run_git(repo, "diff-index", "--quiet", "HEAD", "--", CF_PROJECT_JSON_REL)
|
|
265
|
+
if r.returncode != 0:
|
|
266
|
+
raise RemotePrecheckGitError(
|
|
267
|
+
f"{CF_PROJECT_JSON_REL!r} has uncommitted changes. "
|
|
268
|
+
"Commit or stash before queueing a remote job."
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
|
|
231
272
|
class RemotePushGitError(Exception):
|
|
232
273
|
"""Local repository state is not consistent with origin for remote push."""
|
|
233
274
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|