chipfoundry-cli 2.5.0__tar.gz → 2.5.3__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.0 → chipfoundry_cli-2.5.3}/PKG-INFO +1 -1
- {chipfoundry_cli-2.5.0 → chipfoundry_cli-2.5.3}/chipfoundry_cli/__init__.py +1 -1
- {chipfoundry_cli-2.5.0 → chipfoundry_cli-2.5.3}/chipfoundry_cli/main.py +368 -14
- {chipfoundry_cli-2.5.0 → chipfoundry_cli-2.5.3}/chipfoundry_cli/remote_precheck_git.py +41 -0
- {chipfoundry_cli-2.5.0 → chipfoundry_cli-2.5.3}/pyproject.toml +1 -1
- {chipfoundry_cli-2.5.0 → chipfoundry_cli-2.5.3}/LICENSE +0 -0
- {chipfoundry_cli-2.5.0 → chipfoundry_cli-2.5.3}/README.md +0 -0
- {chipfoundry_cli-2.5.0 → chipfoundry_cli-2.5.3}/chipfoundry_cli/check_refs.py +0 -0
- {chipfoundry_cli-2.5.0 → chipfoundry_cli-2.5.3}/chipfoundry_cli/utils.py +0 -0
- {chipfoundry_cli-2.5.0 → chipfoundry_cli-2.5.3}/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.5.3"
|
|
@@ -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,
|
|
@@ -3587,10 +3591,12 @@ def setup(project_root, repo_owner, repo_name, branch, pdk, caravel_lite,
|
|
|
3587
3591
|
check=True,
|
|
3588
3592
|
capture_output=True
|
|
3589
3593
|
)
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
+
|
|
3595
|
+
venv_python = str(venv_cocotb / 'bin' / 'python3')
|
|
3596
|
+
|
|
3597
|
+
# Install (or reinstall) when the package is missing or --overwrite was used.
|
|
3598
|
+
# The venv may already exist from a previous partial failure.
|
|
3599
|
+
if not is_installed or overwrite:
|
|
3594
3600
|
console.print("[cyan]Installing caravel-cocotb from source (chipfoundry/caravel-sim-infrastructure)...[/cyan]")
|
|
3595
3601
|
subprocess.run(
|
|
3596
3602
|
[venv_python, '-m', 'pip', 'install', '--upgrade', '--no-cache-dir', 'pip'],
|
|
@@ -3695,7 +3701,208 @@ def setup(project_root, repo_owner, repo_name, branch, pdk, caravel_lite,
|
|
|
3695
3701
|
console.print("[bold green]Installation complete![/bold green]")
|
|
3696
3702
|
else:
|
|
3697
3703
|
console.print("[bold green]Setup complete![/bold green]")
|
|
3698
|
-
|
|
3704
|
+
|
|
3705
|
+
|
|
3706
|
+
def _is_nix_toolchain_noise_line(line: str) -> bool:
|
|
3707
|
+
"""Match Nix fetch/build chatter posted by the PNR runner."""
|
|
3708
|
+
s = line.strip()
|
|
3709
|
+
if not s:
|
|
3710
|
+
return True
|
|
3711
|
+
bare = s[len("[stderr]") :].lstrip() if s.startswith("[stderr]") else s
|
|
3712
|
+
low = bare.lower()
|
|
3713
|
+
if low.startswith("copying path"):
|
|
3714
|
+
return True
|
|
3715
|
+
if low.startswith("building '/nix/store/") or low.startswith('building "/nix/store/'):
|
|
3716
|
+
return True
|
|
3717
|
+
if low.startswith("downloading '") or low.startswith('downloading "'):
|
|
3718
|
+
return True
|
|
3719
|
+
if low.startswith("unpacking 'github:") or low.startswith('unpacking "github:'):
|
|
3720
|
+
return True
|
|
3721
|
+
if "paths will be fetched" in low or "paths will be copied" in low:
|
|
3722
|
+
return True
|
|
3723
|
+
if low.startswith("copying ") and " path" in low:
|
|
3724
|
+
return True
|
|
3725
|
+
if low.startswith("waiting for lock") or low.startswith("waiting for locks"):
|
|
3726
|
+
return True
|
|
3727
|
+
if bare.startswith("/nix/store/") or bare.startswith(" /nix/store/"):
|
|
3728
|
+
return True
|
|
3729
|
+
return False
|
|
3730
|
+
|
|
3731
|
+
|
|
3732
|
+
def _print_remote_progress_message(msg: object, *, style: str = "dim") -> None:
|
|
3733
|
+
"""Print worker progress, collapsing Nix toolchain noise into a one-liner."""
|
|
3734
|
+
text = str(msg)
|
|
3735
|
+
lines = text.splitlines()
|
|
3736
|
+
kept: List[str] = []
|
|
3737
|
+
noise = 0
|
|
3738
|
+
for line in lines:
|
|
3739
|
+
if _is_nix_toolchain_noise_line(line):
|
|
3740
|
+
noise += 1
|
|
3741
|
+
else:
|
|
3742
|
+
kept.append(line)
|
|
3743
|
+
if noise and not kept:
|
|
3744
|
+
console.print(
|
|
3745
|
+
Text(
|
|
3746
|
+
f"Nix toolchain resolving… ({noise} build/fetch lines suppressed)",
|
|
3747
|
+
style="dim",
|
|
3748
|
+
)
|
|
3749
|
+
)
|
|
3750
|
+
return
|
|
3751
|
+
if noise:
|
|
3752
|
+
kept.append(f"(also suppressed {noise} Nix build/fetch lines)")
|
|
3753
|
+
out = "\n".join(kept).rstrip()
|
|
3754
|
+
if out:
|
|
3755
|
+
console.print(Text(out, style=style))
|
|
3756
|
+
|
|
3757
|
+
|
|
3758
|
+
def _queue_and_maybe_poll_remote_job(
|
|
3759
|
+
*,
|
|
3760
|
+
create_path: str,
|
|
3761
|
+
job_get_path_template: str,
|
|
3762
|
+
params: list,
|
|
3763
|
+
dry_run: bool,
|
|
3764
|
+
poll: bool,
|
|
3765
|
+
wait_timeout: int,
|
|
3766
|
+
label: str,
|
|
3767
|
+
) -> None:
|
|
3768
|
+
"""POST a remote platform job and optionally poll until terminal status."""
|
|
3769
|
+
import time
|
|
3770
|
+
from urllib.parse import urlencode
|
|
3771
|
+
|
|
3772
|
+
import httpx as httpx_remote
|
|
3773
|
+
|
|
3774
|
+
if dry_run:
|
|
3775
|
+
console.print(f"[cyan]Would POST[/cyan] {create_path}?" + urlencode(params))
|
|
3776
|
+
return
|
|
3777
|
+
if poll and wait_timeout < 0:
|
|
3778
|
+
console.print(
|
|
3779
|
+
"[red]✗[/red] --wait-timeout must be >= 0 (0 means no limit while polling)."
|
|
3780
|
+
)
|
|
3781
|
+
raise SystemExit(1)
|
|
3782
|
+
config = load_user_config()
|
|
3783
|
+
api_key = config.get('api_key')
|
|
3784
|
+
if not api_key:
|
|
3785
|
+
console.print("[yellow]Not logged in.[/yellow] Run [bold]cf login[/bold] first.")
|
|
3786
|
+
raise SystemExit(1)
|
|
3787
|
+
api_url = _get_api_url()
|
|
3788
|
+
client = httpx_remote.Client(
|
|
3789
|
+
base_url=f"{api_url}/api/v1",
|
|
3790
|
+
headers={
|
|
3791
|
+
'Authorization': f'Bearer {api_key}',
|
|
3792
|
+
'User-Agent': _cf_user_agent(),
|
|
3793
|
+
},
|
|
3794
|
+
timeout=120.0,
|
|
3795
|
+
)
|
|
3796
|
+
try:
|
|
3797
|
+
resp = client.post(create_path, params=params)
|
|
3798
|
+
if resp.status_code == 401:
|
|
3799
|
+
console.print("[red]✗[/red] API key is invalid or expired. Run [bold]cf login[/bold].")
|
|
3800
|
+
raise SystemExit(1)
|
|
3801
|
+
if not resp.is_success:
|
|
3802
|
+
try:
|
|
3803
|
+
detail = resp.json().get("detail", resp.text)
|
|
3804
|
+
except Exception:
|
|
3805
|
+
detail = resp.text
|
|
3806
|
+
console.print(f"[red]✗[/red] {detail}")
|
|
3807
|
+
raise SystemExit(1)
|
|
3808
|
+
job = resp.json()
|
|
3809
|
+
jid = job["id"]
|
|
3810
|
+
st0 = job.get("status") or "unknown"
|
|
3811
|
+
if st0 == "failed":
|
|
3812
|
+
console.print(f"[cyan]{label}[/cyan] job_id={jid} status={st0}")
|
|
3813
|
+
elif st0 == "running":
|
|
3814
|
+
console.print(f"[cyan]{label} started[/cyan] job_id={jid} status={st0}")
|
|
3815
|
+
else:
|
|
3816
|
+
console.print(f"[cyan]Queued {label.lower()}[/cyan] job_id={jid} status={st0}")
|
|
3817
|
+
if job.get("status") == "failed" and job.get("error_message"):
|
|
3818
|
+
console.print(f"[red]✗[/red] {job['error_message']}")
|
|
3819
|
+
raise SystemExit(1)
|
|
3820
|
+
if job.get("status") == "completed":
|
|
3821
|
+
console.print(f"[green]✓[/green] {label} completed")
|
|
3822
|
+
if job.get("github_pr_url"):
|
|
3823
|
+
console.print(f" Pull request: {job['github_pr_url']}")
|
|
3824
|
+
return
|
|
3825
|
+
if not poll:
|
|
3826
|
+
console.print(
|
|
3827
|
+
f"[dim]Not waiting: use [bold]--remote --poll[/bold] to stream progress "
|
|
3828
|
+
f"([bold]--wait-timeout 0[/bold] = no time limit while polling).[/dim]"
|
|
3829
|
+
)
|
|
3830
|
+
return
|
|
3831
|
+
deadline = None if wait_timeout == 0 else time.monotonic() + wait_timeout
|
|
3832
|
+
if wait_timeout == 0:
|
|
3833
|
+
console.print("[dim]Polling until the job completes (no timeout).[/dim]")
|
|
3834
|
+
else:
|
|
3835
|
+
console.print(
|
|
3836
|
+
f"[dim]Polling every 5s; stops after {wait_timeout}s if still queued or running. "
|
|
3837
|
+
f"Use [bold]--wait-timeout 0[/bold] for no limit.[/dim]"
|
|
3838
|
+
)
|
|
3839
|
+
last_status_seen = st0
|
|
3840
|
+
terminal = None
|
|
3841
|
+
github_pr_url = None
|
|
3842
|
+
fail_message = None
|
|
3843
|
+
progress_emitted = 0
|
|
3844
|
+
get_path = job_get_path_template.format(jid=jid)
|
|
3845
|
+
console.print("[dim]Worker log batches appear below as the platform receives them (5s poll).[/dim]")
|
|
3846
|
+
while True:
|
|
3847
|
+
if deadline is not None and time.monotonic() > deadline:
|
|
3848
|
+
console.print(
|
|
3849
|
+
f"[yellow]⚠[/yellow] Timed out waiting for {label.lower()} (job still queued or running)."
|
|
3850
|
+
)
|
|
3851
|
+
console.print(
|
|
3852
|
+
f"[dim]job_id={jid} — open the project in the portal or run [bold]cf status[/bold].[/dim]"
|
|
3853
|
+
)
|
|
3854
|
+
console.print(
|
|
3855
|
+
"[dim]Cancel a stuck run in the portal, or retry with e.g. "
|
|
3856
|
+
"[bold]--remote --poll --wait-timeout 14400[/bold].[/dim]"
|
|
3857
|
+
)
|
|
3858
|
+
raise SystemExit(1)
|
|
3859
|
+
time.sleep(5)
|
|
3860
|
+
r2 = client.get(get_path)
|
|
3861
|
+
if r2.status_code == 401:
|
|
3862
|
+
console.print("[red]✗[/red] API key is invalid or expired.")
|
|
3863
|
+
raise SystemExit(1)
|
|
3864
|
+
r2.raise_for_status()
|
|
3865
|
+
j2 = r2.json()
|
|
3866
|
+
st = j2.get("status")
|
|
3867
|
+
prog = j2.get("progress")
|
|
3868
|
+
if isinstance(prog, list) and len(prog) > progress_emitted:
|
|
3869
|
+
for row in prog[progress_emitted:]:
|
|
3870
|
+
if not isinstance(row, dict):
|
|
3871
|
+
continue
|
|
3872
|
+
msg = row.get("message")
|
|
3873
|
+
if msg:
|
|
3874
|
+
_print_remote_progress_message(msg)
|
|
3875
|
+
progress_emitted = len(prog)
|
|
3876
|
+
if st == "completed":
|
|
3877
|
+
terminal = "completed"
|
|
3878
|
+
github_pr_url = j2.get("github_pr_url")
|
|
3879
|
+
break
|
|
3880
|
+
if st == "failed":
|
|
3881
|
+
terminal = "failed"
|
|
3882
|
+
fail_message = j2.get("error_message") or "unknown error"
|
|
3883
|
+
break
|
|
3884
|
+
if st != last_status_seen:
|
|
3885
|
+
console.print(
|
|
3886
|
+
f"[dim]… job status[/dim] [cyan]{st or 'unknown'}[/cyan]"
|
|
3887
|
+
)
|
|
3888
|
+
last_status_seen = st
|
|
3889
|
+
|
|
3890
|
+
if terminal == "completed":
|
|
3891
|
+
console.print(f"[green]✓[/green] {label} completed")
|
|
3892
|
+
if github_pr_url:
|
|
3893
|
+
console.print(f" Pull request: {github_pr_url}")
|
|
3894
|
+
elif terminal == "failed":
|
|
3895
|
+
console.print(f"[red]✗[/red] {label} failed: {fail_message}")
|
|
3896
|
+
raise SystemExit(1)
|
|
3897
|
+
except SystemExit:
|
|
3898
|
+
raise
|
|
3899
|
+
except Exception as e:
|
|
3900
|
+
console.print(f"[red]✗[/red] {label} request failed: {e}")
|
|
3901
|
+
raise SystemExit(1)
|
|
3902
|
+
finally:
|
|
3903
|
+
client.close()
|
|
3904
|
+
|
|
3905
|
+
|
|
3699
3906
|
@main.command(
|
|
3700
3907
|
'harden',
|
|
3701
3908
|
cls=CategorizedCommand,
|
|
@@ -3704,6 +3911,7 @@ def setup(project_root, repo_owner, repo_name, branch, pdk, caravel_lite,
|
|
|
3704
3911
|
("Run Controls", ["tag", "from_step"]),
|
|
3705
3912
|
("GUI Modes", ["open_in_openroad", "open_in_klayout"]),
|
|
3706
3913
|
("Execution Backend", ["pdk", "use_nix", "use_docker"]),
|
|
3914
|
+
("Remote", ["dry_run", "remote", "poll", "git_ref", "wait_timeout"]),
|
|
3707
3915
|
],
|
|
3708
3916
|
)
|
|
3709
3917
|
@click.argument('macro', required=False)
|
|
@@ -3717,8 +3925,46 @@ def setup(project_root, repo_owner, repo_name, branch, pdk, caravel_lite,
|
|
|
3717
3925
|
@click.option('--pdk', help='PDK to use (defaults to sky130A)')
|
|
3718
3926
|
@click.option('--use-nix', is_flag=True, help='Force use of Nix (fails if Nix not available)')
|
|
3719
3927
|
@click.option('--use-docker', is_flag=True, help='Force use of Docker (fails if Docker not available)')
|
|
3720
|
-
|
|
3721
|
-
|
|
3928
|
+
@click.option('--dry-run', is_flag=True, help='Show the configuration without running')
|
|
3929
|
+
@click.option('--remote', is_flag=True, help='Queue PNR on the ChipFoundry platform (requires cf login + linked project)')
|
|
3930
|
+
@click.option(
|
|
3931
|
+
'--poll',
|
|
3932
|
+
is_flag=True,
|
|
3933
|
+
help='With --remote: poll until the job finishes and print progress (5s interval).',
|
|
3934
|
+
)
|
|
3935
|
+
@click.option('--git-ref', default='main', show_default=True, help='Git branch or tag for remote PNR')
|
|
3936
|
+
@click.option(
|
|
3937
|
+
'--wait-timeout',
|
|
3938
|
+
type=int,
|
|
3939
|
+
default=7200,
|
|
3940
|
+
show_default=True,
|
|
3941
|
+
help='With --remote --poll: max seconds to wait (0 = no limit). Ignored without --poll.',
|
|
3942
|
+
)
|
|
3943
|
+
def harden(
|
|
3944
|
+
macro,
|
|
3945
|
+
project_root,
|
|
3946
|
+
list_designs,
|
|
3947
|
+
list_from_steps,
|
|
3948
|
+
tag,
|
|
3949
|
+
from_step,
|
|
3950
|
+
open_in_openroad,
|
|
3951
|
+
open_in_klayout,
|
|
3952
|
+
pdk,
|
|
3953
|
+
use_nix,
|
|
3954
|
+
use_docker,
|
|
3955
|
+
dry_run,
|
|
3956
|
+
remote,
|
|
3957
|
+
poll,
|
|
3958
|
+
git_ref,
|
|
3959
|
+
wait_timeout,
|
|
3960
|
+
):
|
|
3961
|
+
"""Harden a macro using LibreLane (OpenLane 2).
|
|
3962
|
+
|
|
3963
|
+
Examples:
|
|
3964
|
+
cf harden user_proj_example
|
|
3965
|
+
cf harden --list
|
|
3966
|
+
cf harden user_proj_example --remote --poll
|
|
3967
|
+
"""
|
|
3722
3968
|
from datetime import datetime
|
|
3723
3969
|
|
|
3724
3970
|
# If .cf/project.json exists in cwd, use it as default project_root
|
|
@@ -3729,6 +3975,49 @@ def harden(macro, project_root, list_designs, list_from_steps, tag, from_step, o
|
|
|
3729
3975
|
project_root = os.getcwd()
|
|
3730
3976
|
|
|
3731
3977
|
project_root_path = Path(project_root)
|
|
3978
|
+
|
|
3979
|
+
if poll and not remote:
|
|
3980
|
+
console.print("[red]✗[/red] --poll requires --remote.")
|
|
3981
|
+
raise SystemExit(1)
|
|
3982
|
+
|
|
3983
|
+
if remote:
|
|
3984
|
+
if list_designs or not macro:
|
|
3985
|
+
console.print("[red]✗[/red] --remote requires a macro name (cannot use --list).")
|
|
3986
|
+
raise SystemExit(1)
|
|
3987
|
+
if list_from_steps or from_step or open_in_openroad or open_in_klayout:
|
|
3988
|
+
console.print(
|
|
3989
|
+
"[red]✗[/red] --remote cannot be combined with --from, --list-from-steps, or GUI modes."
|
|
3990
|
+
)
|
|
3991
|
+
raise SystemExit(1)
|
|
3992
|
+
if not check_project_initialized(project_root_path, 'harden', dry_run=dry_run, allow_graceful=True):
|
|
3993
|
+
console.print(f"[red]✗[/red] Project not initialized. Please run 'cf init' first.")
|
|
3994
|
+
return
|
|
3995
|
+
platform_id = _load_project_platform_id(str(project_root_path))
|
|
3996
|
+
if not platform_id:
|
|
3997
|
+
console.print(
|
|
3998
|
+
"[red]✗[/red] Link this repo to a platform project (set platform_project_id via [bold]cf link[/bold])."
|
|
3999
|
+
)
|
|
4000
|
+
raise SystemExit(1)
|
|
4001
|
+
try:
|
|
4002
|
+
verify_remote_job_repo(project_root_path, git_ref)
|
|
4003
|
+
except RemotePrecheckGitError as e:
|
|
4004
|
+
console.print(f"[red]✗[/red] {e}")
|
|
4005
|
+
raise SystemExit(1)
|
|
4006
|
+
remote_params = [("macro", macro), ("git_ref", git_ref)]
|
|
4007
|
+
if pdk:
|
|
4008
|
+
remote_params.append(("pdk", pdk))
|
|
4009
|
+
if tag:
|
|
4010
|
+
remote_params.append(("run_tag", tag))
|
|
4011
|
+
_queue_and_maybe_poll_remote_job(
|
|
4012
|
+
create_path=f"/projects/{platform_id}/pnr-jobs",
|
|
4013
|
+
job_get_path_template=f"/projects/{platform_id}/pnr-jobs/{{jid}}",
|
|
4014
|
+
params=remote_params,
|
|
4015
|
+
dry_run=dry_run,
|
|
4016
|
+
poll=poll,
|
|
4017
|
+
wait_timeout=wait_timeout,
|
|
4018
|
+
label="Remote PNR",
|
|
4019
|
+
)
|
|
4020
|
+
return
|
|
3732
4021
|
|
|
3733
4022
|
# Check if project is initialized (skip check for --list or when no macro specified, allow graceful return)
|
|
3734
4023
|
if not list_designs and macro:
|
|
@@ -4094,7 +4383,6 @@ def harden(macro, project_root, list_designs, list_from_steps, tag, from_step, o
|
|
|
4094
4383
|
str(venv_bin / 'python3'), '-m', 'librelane',
|
|
4095
4384
|
'-m', str(project_root_path),
|
|
4096
4385
|
'-m', str(pdk_root),
|
|
4097
|
-
'--dockerized',
|
|
4098
4386
|
]
|
|
4099
4387
|
|
|
4100
4388
|
# Add --docker-no-tty if not running in a TTY (e.g., CI environments)
|
|
@@ -4104,7 +4392,10 @@ def harden(macro, project_root, list_designs, list_from_steps, tag, from_step, o
|
|
|
4104
4392
|
except:
|
|
4105
4393
|
# If we can't detect TTY, assume non-TTY (safer for CI)
|
|
4106
4394
|
cmd.append('--docker-no-tty')
|
|
4107
|
-
|
|
4395
|
+
|
|
4396
|
+
# --docker-no-tty must come before --dockerized
|
|
4397
|
+
cmd.append('--dockerized')
|
|
4398
|
+
|
|
4108
4399
|
cmd.extend([
|
|
4109
4400
|
'--manual-pdk',
|
|
4110
4401
|
'--pdk-root', str(pdk_root),
|
|
@@ -4489,9 +4780,9 @@ def precheck(project_root, skip_checks, magic_drc, checks, list_checks, dry_run,
|
|
|
4489
4780
|
isinstance(det, dict)
|
|
4490
4781
|
and det.get("event") == "check_done"
|
|
4491
4782
|
):
|
|
4492
|
-
|
|
4783
|
+
_print_remote_progress_message(msg, style="bold")
|
|
4493
4784
|
else:
|
|
4494
|
-
|
|
4785
|
+
_print_remote_progress_message(msg)
|
|
4495
4786
|
progress_emitted = len(prog)
|
|
4496
4787
|
if st == "completed":
|
|
4497
4788
|
terminal = "completed"
|
|
@@ -4666,7 +4957,21 @@ def precheck(project_root, skip_checks, magic_drc, checks, list_checks, dry_run,
|
|
|
4666
4957
|
@click.option('--all', 'run_all', is_flag=True, help='Run all tests')
|
|
4667
4958
|
@click.option('--tag', help='Test list tag/yaml file (e.g., all_tests or user_proj_tests)')
|
|
4668
4959
|
@click.option('--dry-run', is_flag=True, help='Show the configuration without running')
|
|
4669
|
-
|
|
4960
|
+
@click.option('--remote', is_flag=True, help='Queue simulation on the ChipFoundry platform (requires cf login + linked project)')
|
|
4961
|
+
@click.option(
|
|
4962
|
+
'--poll',
|
|
4963
|
+
is_flag=True,
|
|
4964
|
+
help='With --remote: poll until the job finishes and print progress (5s interval).',
|
|
4965
|
+
)
|
|
4966
|
+
@click.option('--git-ref', default='main', show_default=True, help='Git branch or tag for remote simulation')
|
|
4967
|
+
@click.option(
|
|
4968
|
+
'--wait-timeout',
|
|
4969
|
+
type=int,
|
|
4970
|
+
default=7200,
|
|
4971
|
+
show_default=True,
|
|
4972
|
+
help='With --remote --poll: max seconds to wait (0 = no limit). Ignored without --poll.',
|
|
4973
|
+
)
|
|
4974
|
+
def verify(test, project_root, sim, list_tests, run_all, tag, dry_run, remote, poll, git_ref, wait_timeout):
|
|
4670
4975
|
"""Run cocotb verification tests.
|
|
4671
4976
|
|
|
4672
4977
|
Examples:
|
|
@@ -4675,6 +4980,8 @@ def verify(test, project_root, sim, list_tests, run_all, tag, dry_run):
|
|
|
4675
4980
|
cf verify counter_la --sim gl # Run gate-level simulation
|
|
4676
4981
|
cf verify --all # Run all tests
|
|
4677
4982
|
cf verify --tag all_tests # Run tests from a yaml list
|
|
4983
|
+
cf verify counter_la --remote
|
|
4984
|
+
cf verify --all --remote --poll
|
|
4678
4985
|
"""
|
|
4679
4986
|
# If .cf/project.json exists in cwd, use it as default project_root
|
|
4680
4987
|
cwd_root, _ = get_project_json_from_cwd()
|
|
@@ -4684,6 +4991,53 @@ def verify(test, project_root, sim, list_tests, run_all, tag, dry_run):
|
|
|
4684
4991
|
project_root = os.getcwd()
|
|
4685
4992
|
|
|
4686
4993
|
project_root_path = Path(project_root)
|
|
4994
|
+
|
|
4995
|
+
if poll and not remote:
|
|
4996
|
+
console.print("[red]✗[/red] --poll requires --remote.")
|
|
4997
|
+
raise SystemExit(1)
|
|
4998
|
+
|
|
4999
|
+
if remote:
|
|
5000
|
+
if list_tests:
|
|
5001
|
+
console.print("[red]✗[/red] --remote cannot be combined with --list.")
|
|
5002
|
+
raise SystemExit(1)
|
|
5003
|
+
if not check_project_initialized(project_root_path, 'verify', dry_run=dry_run, allow_graceful=True):
|
|
5004
|
+
console.print(f"[red]✗[/red] Project not initialized. Please run 'cf init' first.")
|
|
5005
|
+
return
|
|
5006
|
+
if not test and not run_all and not tag:
|
|
5007
|
+
console.print("[red]Error: Specify a test name, use --all, or --tag <test_list>[/red]")
|
|
5008
|
+
raise SystemExit(1)
|
|
5009
|
+
mode_count = int(bool(test)) + int(bool(run_all)) + int(bool(tag))
|
|
5010
|
+
if mode_count != 1:
|
|
5011
|
+
console.print("[red]Error: Use exactly one of: test name, --all, or --tag[/red]")
|
|
5012
|
+
raise SystemExit(1)
|
|
5013
|
+
platform_id = _load_project_platform_id(str(project_root_path))
|
|
5014
|
+
if not platform_id:
|
|
5015
|
+
console.print(
|
|
5016
|
+
"[red]✗[/red] Link this repo to a platform project (set platform_project_id via [bold]cf link[/bold])."
|
|
5017
|
+
)
|
|
5018
|
+
raise SystemExit(1)
|
|
5019
|
+
try:
|
|
5020
|
+
verify_remote_job_repo(project_root_path, git_ref)
|
|
5021
|
+
except RemotePrecheckGitError as e:
|
|
5022
|
+
console.print(f"[red]✗[/red] {e}")
|
|
5023
|
+
raise SystemExit(1)
|
|
5024
|
+
remote_params = [("git_ref", git_ref), ("sim_type", (sim or "rtl").lower())]
|
|
5025
|
+
if run_all:
|
|
5026
|
+
remote_params.append(("run_all", "true"))
|
|
5027
|
+
elif tag:
|
|
5028
|
+
remote_params.append(("test_list_tag", tag))
|
|
5029
|
+
else:
|
|
5030
|
+
remote_params.append(("test", test))
|
|
5031
|
+
_queue_and_maybe_poll_remote_job(
|
|
5032
|
+
create_path=f"/projects/{platform_id}/simulation-jobs",
|
|
5033
|
+
job_get_path_template=f"/projects/{platform_id}/simulation-jobs/{{jid}}",
|
|
5034
|
+
params=remote_params,
|
|
5035
|
+
dry_run=dry_run,
|
|
5036
|
+
poll=poll,
|
|
5037
|
+
wait_timeout=wait_timeout,
|
|
5038
|
+
label="Remote simulation",
|
|
5039
|
+
)
|
|
5040
|
+
return
|
|
4687
5041
|
|
|
4688
5042
|
# Check if project is initialized (skip check if just listing tests, allow graceful return)
|
|
4689
5043
|
if not list_tests:
|
|
@@ -5392,4 +5746,4 @@ def whoami_cmd():
|
|
|
5392
5746
|
|
|
5393
5747
|
|
|
5394
5748
|
if __name__ == "__main__":
|
|
5395
|
-
main()
|
|
5749
|
+
main()
|
|
@@ -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
|