chipfoundry-cli 2.3.0__tar.gz → 2.3.12__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.3.0 → chipfoundry_cli-2.3.12}/PKG-INFO +1 -1
- {chipfoundry_cli-2.3.0 → chipfoundry_cli-2.3.12}/chipfoundry_cli/__init__.py +1 -1
- {chipfoundry_cli-2.3.0 → chipfoundry_cli-2.3.12}/chipfoundry_cli/main.py +274 -8
- chipfoundry_cli-2.3.12/chipfoundry_cli/remote_precheck_git.py +228 -0
- {chipfoundry_cli-2.3.0 → chipfoundry_cli-2.3.12}/pyproject.toml +1 -1
- {chipfoundry_cli-2.3.0 → chipfoundry_cli-2.3.12}/LICENSE +0 -0
- {chipfoundry_cli-2.3.0 → chipfoundry_cli-2.3.12}/README.md +0 -0
- {chipfoundry_cli-2.3.0 → chipfoundry_cli-2.3.12}/chipfoundry_cli/utils.py +0 -0
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""ChipFoundry CLI package: Automate project submission to SFTP."""
|
|
2
|
-
__version__ = "
|
|
2
|
+
__version__ = "2.3.2"
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import click
|
|
2
2
|
import getpass
|
|
3
|
+
from chipfoundry_cli.remote_precheck_git import RemotePrecheckGitError, verify_remote_precheck_repo
|
|
3
4
|
from chipfoundry_cli.utils import (
|
|
4
5
|
collect_project_files, ensure_cf_directory, update_or_create_project_json,
|
|
5
6
|
sftp_connect, upload_with_progress, sftp_ensure_dirs, sftp_download_recursive,
|
|
@@ -1653,6 +1654,13 @@ STATUS_HINTS = {
|
|
|
1653
1654
|
"CHANGES_REQUESTED": "Changes requested by the review team. See notes above.",
|
|
1654
1655
|
}
|
|
1655
1656
|
|
|
1657
|
+
REMOTE_PRECHECK_STATUS_COLORS = {
|
|
1658
|
+
"queued": "yellow",
|
|
1659
|
+
"running": "cyan bold",
|
|
1660
|
+
"completed": "green",
|
|
1661
|
+
"failed": "red",
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1656
1664
|
|
|
1657
1665
|
def _show_platform_status(project_root: str):
|
|
1658
1666
|
"""Show the platform pipeline panel if the project is linked. Returns True if shown."""
|
|
@@ -1695,6 +1703,28 @@ def _show_platform_status(project_root: str):
|
|
|
1695
1703
|
lines.append(f"[bold]Tapeout:[/bold] [{tc}]{tl}[/{tc}]")
|
|
1696
1704
|
if project.get('gds_hash'):
|
|
1697
1705
|
lines.append(f"[bold]GDS Hash:[/bold] {project['gds_hash'][:16]}...")
|
|
1706
|
+
rj = project.get("latest_remote_precheck_job")
|
|
1707
|
+
if isinstance(rj, dict) and rj.get("status"):
|
|
1708
|
+
jst = str(rj.get("status", ""))
|
|
1709
|
+
jc = REMOTE_PRECHECK_STATUS_COLORS.get(jst, "white")
|
|
1710
|
+
lines.append(f"[bold]Remote precheck:[/bold] [{jc}]{jst}[/{jc}]")
|
|
1711
|
+
ref = rj.get("git_ref")
|
|
1712
|
+
if ref:
|
|
1713
|
+
lines.append(f"[dim] git ref: {ref}[/dim]")
|
|
1714
|
+
created = rj.get("created_at")
|
|
1715
|
+
if created and isinstance(created, str):
|
|
1716
|
+
lines.append(f"[dim] started: {created[:19]}[/dim]")
|
|
1717
|
+
if jst in ("completed", "failed"):
|
|
1718
|
+
done = rj.get("completed_at")
|
|
1719
|
+
if done and isinstance(done, str):
|
|
1720
|
+
lines.append(f"[dim] finished: {done[:19]}[/dim]")
|
|
1721
|
+
if jst == "failed" and rj.get("error_message"):
|
|
1722
|
+
err = str(rj["error_message"])
|
|
1723
|
+
if len(err) > 240:
|
|
1724
|
+
err = err[:237] + "..."
|
|
1725
|
+
lines.append(f"[red] {err}[/red]")
|
|
1726
|
+
if jst == "completed" and rj.get("github_pr_url"):
|
|
1727
|
+
lines.append(f"[green] PR:[/green] {rj['github_pr_url']}")
|
|
1698
1728
|
if project.get('updated_at'):
|
|
1699
1729
|
lines.append(f"[bold]Updated:[/bold] {project['updated_at'][:10]}")
|
|
1700
1730
|
if project.get('admin_review_notes'):
|
|
@@ -3220,13 +3250,49 @@ def repo_update(project_root, repo_owner, repo_name, branch, dry_run):
|
|
|
3220
3250
|
raise click.Abort()
|
|
3221
3251
|
|
|
3222
3252
|
|
|
3253
|
+
def _upload_precheck_results(project_json_path: Path):
|
|
3254
|
+
"""Upload precheck results to the platform (best-effort, never fatal)."""
|
|
3255
|
+
try:
|
|
3256
|
+
with open(project_json_path, "r") as f:
|
|
3257
|
+
pj = json.load(f)
|
|
3258
|
+
precheck_blob = pj.get("precheck")
|
|
3259
|
+
if not precheck_blob:
|
|
3260
|
+
return
|
|
3261
|
+
platform_id = pj.get("project", {}).get("platform_project_id")
|
|
3262
|
+
if not platform_id:
|
|
3263
|
+
return
|
|
3264
|
+
config = load_user_config()
|
|
3265
|
+
if not config.get("api_key"):
|
|
3266
|
+
return
|
|
3267
|
+
_api_put(f"/projects/{platform_id}", {"precheck_results": precheck_blob})
|
|
3268
|
+
console.print("[green]✓ Precheck results synced to platform[/green]")
|
|
3269
|
+
except SystemExit:
|
|
3270
|
+
console.print("[yellow]⚠ Precheck results could not be synced to platform[/yellow]")
|
|
3271
|
+
except Exception:
|
|
3272
|
+
console.print("[yellow]⚠ Precheck results could not be synced to platform[/yellow]")
|
|
3273
|
+
|
|
3274
|
+
|
|
3223
3275
|
@main.command('precheck')
|
|
3224
3276
|
@click.option('--project-root', type=click.Path(exists=True, file_okay=False), help='Path to the project directory (defaults to current directory)')
|
|
3225
3277
|
@click.option('--skip-checks', multiple=True, help='Checks to skip (can be specified multiple times)')
|
|
3226
3278
|
@click.option('--magic-drc', is_flag=True, help='Include Magic DRC check (optional, off by default)')
|
|
3227
3279
|
@click.option('--checks', multiple=True, help='Specific checks to run (can be specified multiple times)')
|
|
3228
3280
|
@click.option('--dry-run', is_flag=True, help='Show the command without running')
|
|
3229
|
-
|
|
3281
|
+
@click.option('--remote', is_flag=True, help='Queue precheck on the chipIgnite platform (requires cf login + linked project)')
|
|
3282
|
+
@click.option(
|
|
3283
|
+
'--poll',
|
|
3284
|
+
is_flag=True,
|
|
3285
|
+
help='With --remote: poll until the job finishes and print progress (5s interval).',
|
|
3286
|
+
)
|
|
3287
|
+
@click.option('--git-ref', default='main', show_default=True, help='Git branch or tag for remote precheck')
|
|
3288
|
+
@click.option(
|
|
3289
|
+
'--wait-timeout',
|
|
3290
|
+
type=int,
|
|
3291
|
+
default=7200,
|
|
3292
|
+
show_default=True,
|
|
3293
|
+
help='With --remote --poll: max seconds to wait (0 = no limit). Ignored without --poll.',
|
|
3294
|
+
)
|
|
3295
|
+
def precheck(project_root, skip_checks, magic_drc, checks, dry_run, remote, poll, git_ref, wait_timeout):
|
|
3230
3296
|
"""Run precheck validation on the project.
|
|
3231
3297
|
|
|
3232
3298
|
This runs the cf-precheck tool to validate your design before
|
|
@@ -3237,6 +3303,13 @@ def precheck(project_root, skip_checks, magic_drc, checks, dry_run):
|
|
|
3237
3303
|
cf precheck --skip-checks lvs # Skip LVS check
|
|
3238
3304
|
cf precheck --magic-drc # Include optional Magic DRC
|
|
3239
3305
|
cf precheck --checks topcell_check # Run specific checks only
|
|
3306
|
+
cf precheck --remote # Queue on platform; exit when accepted
|
|
3307
|
+
cf precheck --remote --poll # Wait and stream progress
|
|
3308
|
+
cf precheck --remote --poll --wait-timeout 0 # Poll until done (no time limit)
|
|
3309
|
+
|
|
3310
|
+
Remote precheck requires your local HEAD to match origin for --git-ref, and precheck
|
|
3311
|
+
inputs (wrapper GDS, verilog/rtl/user_defines.v when the GPIO check runs, and tracked
|
|
3312
|
+
.cf/project.json) to match that commit.
|
|
3240
3313
|
"""
|
|
3241
3314
|
cwd_root, _ = get_project_json_from_cwd()
|
|
3242
3315
|
if not project_root and cwd_root:
|
|
@@ -3252,6 +3325,180 @@ def precheck(project_root, skip_checks, magic_drc, checks, dry_run):
|
|
|
3252
3325
|
return
|
|
3253
3326
|
|
|
3254
3327
|
project_json_path = project_root_path / '.cf' / 'project.json'
|
|
3328
|
+
|
|
3329
|
+
if poll and not remote:
|
|
3330
|
+
console.print("[red]✗[/red] --poll requires --remote.")
|
|
3331
|
+
raise SystemExit(1)
|
|
3332
|
+
|
|
3333
|
+
if remote:
|
|
3334
|
+
import time
|
|
3335
|
+
from urllib.parse import urlencode
|
|
3336
|
+
|
|
3337
|
+
import httpx as httpx_remote
|
|
3338
|
+
platform_id = _load_project_platform_id(str(project_root_path))
|
|
3339
|
+
if not platform_id:
|
|
3340
|
+
console.print(
|
|
3341
|
+
"[red]✗[/red] Link this repo to a platform project (set platform_project_id via [bold]cf link[/bold])."
|
|
3342
|
+
)
|
|
3343
|
+
raise SystemExit(1)
|
|
3344
|
+
try:
|
|
3345
|
+
verify_remote_precheck_repo(
|
|
3346
|
+
project_root_path,
|
|
3347
|
+
git_ref,
|
|
3348
|
+
checks=tuple(checks),
|
|
3349
|
+
skip_checks=tuple(skip_checks),
|
|
3350
|
+
)
|
|
3351
|
+
except RemotePrecheckGitError as e:
|
|
3352
|
+
console.print(f"[red]✗[/red] {e}")
|
|
3353
|
+
raise SystemExit(1)
|
|
3354
|
+
remote_params = [("git_ref", git_ref)]
|
|
3355
|
+
# Single checks= / skip_checks= value so proxies do not drop duplicate query keys.
|
|
3356
|
+
if checks:
|
|
3357
|
+
remote_params.append(("checks", ",".join(checks)))
|
|
3358
|
+
if skip_checks:
|
|
3359
|
+
remote_params.append(("skip_checks", ",".join(skip_checks)))
|
|
3360
|
+
if magic_drc:
|
|
3361
|
+
remote_params.append(("magic_drc", "true"))
|
|
3362
|
+
if dry_run:
|
|
3363
|
+
console.print(
|
|
3364
|
+
f"[cyan]Would POST[/cyan] /projects/{platform_id}/precheck-jobs?"
|
|
3365
|
+
+ urlencode(remote_params)
|
|
3366
|
+
)
|
|
3367
|
+
return
|
|
3368
|
+
if poll and wait_timeout < 0:
|
|
3369
|
+
console.print(
|
|
3370
|
+
"[red]✗[/red] --wait-timeout must be >= 0 (0 means no limit while polling)."
|
|
3371
|
+
)
|
|
3372
|
+
raise SystemExit(1)
|
|
3373
|
+
config = load_user_config()
|
|
3374
|
+
api_key = config.get('api_key')
|
|
3375
|
+
if not api_key:
|
|
3376
|
+
console.print("[yellow]Not logged in.[/yellow] Run [bold]cf login[/bold] first.")
|
|
3377
|
+
raise SystemExit(1)
|
|
3378
|
+
api_url = _get_api_url()
|
|
3379
|
+
client = httpx_remote.Client(
|
|
3380
|
+
base_url=f"{api_url}/api/v1",
|
|
3381
|
+
headers={'Authorization': f'Bearer {api_key}'},
|
|
3382
|
+
timeout=120.0,
|
|
3383
|
+
)
|
|
3384
|
+
try:
|
|
3385
|
+
resp = client.post(
|
|
3386
|
+
f"/projects/{platform_id}/precheck-jobs",
|
|
3387
|
+
params=remote_params,
|
|
3388
|
+
)
|
|
3389
|
+
if resp.status_code == 401:
|
|
3390
|
+
console.print("[red]✗[/red] API key is invalid or expired. Run [bold]cf login[/bold].")
|
|
3391
|
+
raise SystemExit(1)
|
|
3392
|
+
if not resp.is_success:
|
|
3393
|
+
try:
|
|
3394
|
+
detail = resp.json().get("detail", resp.text)
|
|
3395
|
+
except Exception:
|
|
3396
|
+
detail = resp.text
|
|
3397
|
+
console.print(f"[red]✗[/red] {detail}")
|
|
3398
|
+
raise SystemExit(1)
|
|
3399
|
+
job = resp.json()
|
|
3400
|
+
jid = job["id"]
|
|
3401
|
+
st0 = job.get("status") or "unknown"
|
|
3402
|
+
if st0 == "failed":
|
|
3403
|
+
console.print(f"[cyan]Remote precheck[/cyan] job_id={jid} status={st0}")
|
|
3404
|
+
elif st0 == "running":
|
|
3405
|
+
console.print(f"[cyan]Remote precheck started[/cyan] job_id={jid} status={st0}")
|
|
3406
|
+
else:
|
|
3407
|
+
console.print(f"[cyan]Queued remote precheck[/cyan] job_id={jid} status={st0}")
|
|
3408
|
+
if job.get("status") == "failed" and job.get("error_message"):
|
|
3409
|
+
console.print(f"[red]✗[/red] {job['error_message']}")
|
|
3410
|
+
raise SystemExit(1)
|
|
3411
|
+
if job.get("status") == "completed":
|
|
3412
|
+
console.print("[green]✓[/green] Remote precheck completed")
|
|
3413
|
+
if job.get("github_pr_url"):
|
|
3414
|
+
console.print(f" Pull request: {job['github_pr_url']}")
|
|
3415
|
+
return
|
|
3416
|
+
if not poll:
|
|
3417
|
+
console.print(
|
|
3418
|
+
"[dim]Not waiting: use [bold]cf precheck --remote --poll[/bold] to stream progress "
|
|
3419
|
+
"([bold]--wait-timeout 0[/bold] = no time limit while polling).[/dim]"
|
|
3420
|
+
)
|
|
3421
|
+
return
|
|
3422
|
+
deadline = None if wait_timeout == 0 else time.monotonic() + wait_timeout
|
|
3423
|
+
if wait_timeout == 0:
|
|
3424
|
+
console.print("[dim]Polling until the job completes (no timeout).[/dim]")
|
|
3425
|
+
else:
|
|
3426
|
+
console.print(
|
|
3427
|
+
f"[dim]Polling every 5s; stops after {wait_timeout}s if still queued or running. "
|
|
3428
|
+
f"Use [bold]--wait-timeout 0[/bold] for no limit.[/dim]"
|
|
3429
|
+
)
|
|
3430
|
+
last_status_seen = st0
|
|
3431
|
+
terminal = None
|
|
3432
|
+
github_pr_url = None
|
|
3433
|
+
fail_message = None
|
|
3434
|
+
progress_emitted = 0
|
|
3435
|
+
console.print("[dim]Worker log batches appear below as the platform receives them (5s poll).[/dim]")
|
|
3436
|
+
while True:
|
|
3437
|
+
if deadline is not None and time.monotonic() > deadline:
|
|
3438
|
+
console.print(
|
|
3439
|
+
"[yellow]⚠[/yellow] Timed out waiting for remote precheck (job still queued or running)."
|
|
3440
|
+
)
|
|
3441
|
+
console.print(
|
|
3442
|
+
f"[dim]job_id={jid} — open the project in the portal or run [bold]cf status[/bold].[/dim]"
|
|
3443
|
+
)
|
|
3444
|
+
console.print(
|
|
3445
|
+
"[dim]Cancel a stuck run in the portal, or retry with e.g. "
|
|
3446
|
+
"[bold]cf precheck --remote --poll --wait-timeout 14400[/bold].[/dim]"
|
|
3447
|
+
)
|
|
3448
|
+
raise SystemExit(1)
|
|
3449
|
+
time.sleep(5)
|
|
3450
|
+
r2 = client.get(f"/projects/{platform_id}/precheck-jobs/{jid}")
|
|
3451
|
+
if r2.status_code == 401:
|
|
3452
|
+
console.print("[red]✗[/red] API key is invalid or expired.")
|
|
3453
|
+
raise SystemExit(1)
|
|
3454
|
+
r2.raise_for_status()
|
|
3455
|
+
j2 = r2.json()
|
|
3456
|
+
st = j2.get("status")
|
|
3457
|
+
prog = j2.get("progress")
|
|
3458
|
+
if isinstance(prog, list) and len(prog) > progress_emitted:
|
|
3459
|
+
for row in prog[progress_emitted:]:
|
|
3460
|
+
if not isinstance(row, dict):
|
|
3461
|
+
continue
|
|
3462
|
+
msg = row.get("message")
|
|
3463
|
+
if msg:
|
|
3464
|
+
det = row.get("details")
|
|
3465
|
+
if (
|
|
3466
|
+
isinstance(det, dict)
|
|
3467
|
+
and det.get("event") == "check_done"
|
|
3468
|
+
):
|
|
3469
|
+
console.print(Text(str(msg), style="bold"))
|
|
3470
|
+
else:
|
|
3471
|
+
console.print(Text(str(msg), style="dim"))
|
|
3472
|
+
progress_emitted = len(prog)
|
|
3473
|
+
if st == "completed":
|
|
3474
|
+
terminal = "completed"
|
|
3475
|
+
github_pr_url = j2.get("github_pr_url")
|
|
3476
|
+
break
|
|
3477
|
+
if st == "failed":
|
|
3478
|
+
terminal = "failed"
|
|
3479
|
+
fail_message = j2.get("error_message") or "unknown error"
|
|
3480
|
+
break
|
|
3481
|
+
if st != last_status_seen:
|
|
3482
|
+
console.print(
|
|
3483
|
+
f"[dim]… job status[/dim] [cyan]{st or 'unknown'}[/cyan]"
|
|
3484
|
+
)
|
|
3485
|
+
last_status_seen = st
|
|
3486
|
+
|
|
3487
|
+
if terminal == "completed":
|
|
3488
|
+
console.print("[green]✓[/green] Remote precheck completed")
|
|
3489
|
+
if github_pr_url:
|
|
3490
|
+
console.print(f" Pull request: {github_pr_url}")
|
|
3491
|
+
elif terminal == "failed":
|
|
3492
|
+
console.print(f"[red]✗[/red] Remote precheck failed: {fail_message}")
|
|
3493
|
+
raise SystemExit(1)
|
|
3494
|
+
except SystemExit:
|
|
3495
|
+
raise
|
|
3496
|
+
except Exception as e:
|
|
3497
|
+
console.print(f"[red]✗[/red] Remote precheck request failed: {e}")
|
|
3498
|
+
raise SystemExit(1)
|
|
3499
|
+
finally:
|
|
3500
|
+
client.close()
|
|
3501
|
+
return
|
|
3255
3502
|
|
|
3256
3503
|
with open(project_json_path, 'r') as f:
|
|
3257
3504
|
project_data = json.load(f)
|
|
@@ -3297,17 +3544,20 @@ def precheck(project_root, skip_checks, magic_drc, checks, dry_run):
|
|
|
3297
3544
|
|
|
3298
3545
|
if magic_drc:
|
|
3299
3546
|
precheck_args.append('--magic-drc')
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3547
|
+
|
|
3548
|
+
# Positional check names before --skip-checks (matches cf-precheck argparse; see
|
|
3549
|
+
# precheck-runner _cf_precheck_shell_cmd).
|
|
3304
3550
|
if checks:
|
|
3305
3551
|
precheck_args.extend(list(checks))
|
|
3552
|
+
|
|
3553
|
+
if skip_checks:
|
|
3554
|
+
precheck_args.extend(['--skip-checks'] + list(skip_checks))
|
|
3306
3555
|
|
|
3307
3556
|
inner_cmd = 'pip3 install --upgrade -q --root-user-action=ignore cf-precheck 2>/dev/null && exec cf-precheck ' + ' '.join(precheck_args)
|
|
3308
3557
|
|
|
3309
3558
|
docker_cmd = [
|
|
3310
3559
|
'docker', 'run', '--rm', '--init',
|
|
3560
|
+
'--platform', 'linux/amd64',
|
|
3311
3561
|
'-v', f'{project_root_path}:{project_root_path}',
|
|
3312
3562
|
'-v', f'{pdk_root}:{pdk_root}',
|
|
3313
3563
|
'-e', f'PDK_ROOT={pdk_root}',
|
|
@@ -3338,7 +3588,7 @@ def precheck(project_root, skip_checks, magic_drc, checks, dry_run):
|
|
|
3338
3588
|
console.print(f"[cyan]Checking for Docker image updates...[/cyan]")
|
|
3339
3589
|
try:
|
|
3340
3590
|
subprocess.run(
|
|
3341
|
-
['docker', 'pull', docker_image],
|
|
3591
|
+
['docker', 'pull', '--platform', 'linux/amd64', docker_image],
|
|
3342
3592
|
check=True,
|
|
3343
3593
|
capture_output=True,
|
|
3344
3594
|
)
|
|
@@ -3368,6 +3618,10 @@ def precheck(project_root, skip_checks, magic_drc, checks, dry_run):
|
|
|
3368
3618
|
sys.exit(130)
|
|
3369
3619
|
else:
|
|
3370
3620
|
console.print(f"[red]✗[/red] Precheck failed (exit code {returncode})")
|
|
3621
|
+
|
|
3622
|
+
_upload_precheck_results(project_json_path)
|
|
3623
|
+
|
|
3624
|
+
if returncode != 0:
|
|
3371
3625
|
sys.exit(returncode)
|
|
3372
3626
|
|
|
3373
3627
|
except KeyboardInterrupt:
|
|
@@ -3727,7 +3981,7 @@ def _api_put(path: str, json_data: dict):
|
|
|
3727
3981
|
client.close()
|
|
3728
3982
|
|
|
3729
3983
|
|
|
3730
|
-
_SYNC_KEEP_KEYS = {"project", "tapeout"}
|
|
3984
|
+
_SYNC_KEEP_KEYS = {"project", "tapeout", "precheck"}
|
|
3731
3985
|
|
|
3732
3986
|
|
|
3733
3987
|
def _slim_project_json(pj: dict) -> dict:
|
|
@@ -3862,13 +4116,25 @@ def unlink_cmd():
|
|
|
3862
4116
|
console.print("[green]✓ Platform link removed.[/green] The remote project is not deleted.")
|
|
3863
4117
|
|
|
3864
4118
|
|
|
4119
|
+
DEV_API_URL = 'https://dev-api.chipfoundry.io'
|
|
4120
|
+
|
|
4121
|
+
|
|
3865
4122
|
@main.command('login')
|
|
3866
|
-
|
|
4123
|
+
@click.option('--test', is_flag=True, help='Authenticate against the dev/test platform')
|
|
4124
|
+
def login_cmd(test):
|
|
3867
4125
|
"""Authenticate with ChipFoundry platform via browser."""
|
|
3868
4126
|
import httpx
|
|
3869
4127
|
import webbrowser
|
|
3870
4128
|
import time
|
|
3871
4129
|
|
|
4130
|
+
config = load_user_config()
|
|
4131
|
+
if test:
|
|
4132
|
+
config['api_url'] = DEV_API_URL
|
|
4133
|
+
save_user_config(config)
|
|
4134
|
+
elif config.get('api_url') == DEV_API_URL:
|
|
4135
|
+
del config['api_url']
|
|
4136
|
+
save_user_config(config)
|
|
4137
|
+
|
|
3872
4138
|
api_url = _get_api_url()
|
|
3873
4139
|
console.print("[bold cyan]ChipFoundry CLI Login[/bold cyan]")
|
|
3874
4140
|
console.print(f"Opening browser to authenticate with [bold]{api_url}[/bold]...\n")
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Git consistency checks before queueing remote precheck.
|
|
3
|
+
|
|
4
|
+
Ensures local HEAD matches the commit the platform will clone for --git-ref and that
|
|
5
|
+
working tree / index match that commit for precheck inputs (wrapper GDS, user_defines.v
|
|
6
|
+
when the GPIO check runs, and .cf/project.json when it is tracked).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import subprocess
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import List, Optional, Set, Tuple
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class RemotePrecheckGitError(Exception):
|
|
18
|
+
"""Local repository state is not consistent with origin for remote precheck."""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
_GDS_BASES: Tuple[Tuple[str, str], ...] = (
|
|
22
|
+
("analog", "gds/user_analog_project_wrapper"),
|
|
23
|
+
("digital", "gds/user_project_wrapper"),
|
|
24
|
+
("openframe", "gds/openframe_project_wrapper"),
|
|
25
|
+
("mini", "gds/user_project_wrapper_mini4"),
|
|
26
|
+
)
|
|
27
|
+
_GDS_SUFFIXES: Tuple[str, ...] = (".gds", ".gds.gz")
|
|
28
|
+
|
|
29
|
+
USER_DEFINES_REL = "verilog/rtl/user_defines.v"
|
|
30
|
+
CF_PROJECT_JSON_REL = ".cf/project.json"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _run_git(repo: Path, *args: str, timeout: float = 120.0) -> subprocess.CompletedProcess[str]:
|
|
34
|
+
return subprocess.run(
|
|
35
|
+
["git", *args],
|
|
36
|
+
cwd=str(repo),
|
|
37
|
+
capture_output=True,
|
|
38
|
+
text=True,
|
|
39
|
+
timeout=timeout,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _resolve_origin_tip_sha(repo: Path, git_ref: str) -> str:
|
|
44
|
+
ref = git_ref.strip()
|
|
45
|
+
if not ref:
|
|
46
|
+
raise RemotePrecheckGitError("--git-ref must be a non-empty branch or tag name.")
|
|
47
|
+
r = _run_git(repo, "ls-remote", "origin", f"refs/heads/{ref}")
|
|
48
|
+
if r.returncode != 0:
|
|
49
|
+
raise RemotePrecheckGitError(
|
|
50
|
+
f"git ls-remote failed (is 'origin' configured and reachable?): {r.stderr.strip() or r.stdout}"
|
|
51
|
+
)
|
|
52
|
+
lines = [ln for ln in r.stdout.strip().splitlines() if ln.strip()]
|
|
53
|
+
if lines:
|
|
54
|
+
return lines[0].split()[0]
|
|
55
|
+
r2 = _run_git(repo, "ls-remote", "origin", f"refs/tags/{ref}")
|
|
56
|
+
if r2.returncode != 0:
|
|
57
|
+
raise RemotePrecheckGitError(
|
|
58
|
+
f"git ls-remote failed for tags: {r2.stderr.strip() or r2.stdout}"
|
|
59
|
+
)
|
|
60
|
+
lines2 = [ln for ln in r2.stdout.strip().splitlines() if ln.strip()]
|
|
61
|
+
if lines2:
|
|
62
|
+
return lines2[0].split()[0]
|
|
63
|
+
raise RemotePrecheckGitError(
|
|
64
|
+
f"No branch or tag {ref!r} found on origin. Push the ref or fix --git-ref."
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _local_head_sha(repo: Path) -> str:
|
|
69
|
+
r = _run_git(repo, "rev-parse", "HEAD")
|
|
70
|
+
if r.returncode != 0:
|
|
71
|
+
raise RemotePrecheckGitError(
|
|
72
|
+
f"Not a valid git checkout: {r.stderr.strip() or 'git rev-parse HEAD failed'}"
|
|
73
|
+
)
|
|
74
|
+
return r.stdout.strip()
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _detect_wrapper_gds(repo: Path) -> Tuple[str, str]:
|
|
78
|
+
"""Return (project_kind, relative_path) for the single wrapper GDS, or raise."""
|
|
79
|
+
hits: list[Tuple[str, str]] = []
|
|
80
|
+
for kind, base in _GDS_BASES:
|
|
81
|
+
for suf in _GDS_SUFFIXES:
|
|
82
|
+
rel = base + suf
|
|
83
|
+
if (repo / rel).is_file():
|
|
84
|
+
hits.append((kind, rel))
|
|
85
|
+
break
|
|
86
|
+
if not hits:
|
|
87
|
+
raise RemotePrecheckGitError(
|
|
88
|
+
"No wrapper GDS found (expected exactly one of e.g. "
|
|
89
|
+
"gds/user_project_wrapper.gds, gds/user_analog_project_wrapper.gds, …). "
|
|
90
|
+
"Remote precheck requires the same layout as local cf-precheck."
|
|
91
|
+
)
|
|
92
|
+
if len(hits) > 1:
|
|
93
|
+
paths = ", ".join(h[1] for h in hits)
|
|
94
|
+
raise RemotePrecheckGitError(
|
|
95
|
+
f"Multiple wrapper GDS layouts found ({paths}). Remove extras so only one project type is present."
|
|
96
|
+
)
|
|
97
|
+
return hits[0]
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _load_cf_project_type(project_json: Path) -> Optional[str]:
|
|
101
|
+
if not project_json.is_file():
|
|
102
|
+
return None
|
|
103
|
+
try:
|
|
104
|
+
data = json.loads(project_json.read_text(encoding="utf-8"))
|
|
105
|
+
except (json.JSONDecodeError, OSError):
|
|
106
|
+
return None
|
|
107
|
+
t = data.get("project", {}).get("type")
|
|
108
|
+
return str(t).strip().lower() if t else None
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _gpio_defines_will_run(
|
|
112
|
+
checks: Tuple[str, ...],
|
|
113
|
+
skip_checks: Tuple[str, ...],
|
|
114
|
+
project_kind: str,
|
|
115
|
+
) -> bool:
|
|
116
|
+
if project_kind not in ("analog", "digital"):
|
|
117
|
+
return False
|
|
118
|
+
cnorm = {x.strip().lower() for x in checks if x.strip()}
|
|
119
|
+
snorm = {x.strip().lower() for x in skip_checks if x.strip()}
|
|
120
|
+
if "gpio_defines" in snorm:
|
|
121
|
+
return False
|
|
122
|
+
if cnorm:
|
|
123
|
+
return "gpio_defines" in cnorm
|
|
124
|
+
return True
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _path_tracked_in_git(repo: Path, rel: str) -> bool:
|
|
128
|
+
r = _run_git(repo, "ls-files", "--error-unmatch", rel)
|
|
129
|
+
return r.returncode == 0
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _critical_precheck_paths(
|
|
133
|
+
repo: Path,
|
|
134
|
+
project_json: Path,
|
|
135
|
+
checks: Tuple[str, ...],
|
|
136
|
+
skip_checks: Tuple[str, ...],
|
|
137
|
+
) -> Set[str]:
|
|
138
|
+
kind_gds, gds_rel = _detect_wrapper_gds(repo)
|
|
139
|
+
out: Set[str] = {gds_rel}
|
|
140
|
+
|
|
141
|
+
cf_type = _load_cf_project_type(project_json)
|
|
142
|
+
if cf_type and cf_type != kind_gds:
|
|
143
|
+
raise RemotePrecheckGitError(
|
|
144
|
+
f".cf/project.json type is {cf_type!r} but the wrapper GDS indicates {kind_gds!r}. "
|
|
145
|
+
"Fix project type or GDS layout before remote precheck."
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
if _gpio_defines_will_run(checks, skip_checks, kind_gds):
|
|
149
|
+
ud = repo / USER_DEFINES_REL
|
|
150
|
+
if ud.is_file() or _path_tracked_in_git(repo, USER_DEFINES_REL):
|
|
151
|
+
out.add(USER_DEFINES_REL)
|
|
152
|
+
|
|
153
|
+
if _path_tracked_in_git(repo, CF_PROJECT_JSON_REL):
|
|
154
|
+
out.add(CF_PROJECT_JSON_REL)
|
|
155
|
+
|
|
156
|
+
return out
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _porcelain_paths(repo: Path) -> List[str]:
|
|
160
|
+
r = _run_git(repo, "status", "--porcelain=v1", "-u")
|
|
161
|
+
if r.returncode != 0:
|
|
162
|
+
raise RemotePrecheckGitError(f"git status failed: {r.stderr.strip()}")
|
|
163
|
+
paths: List[str] = []
|
|
164
|
+
for line in r.stdout.splitlines():
|
|
165
|
+
if len(line) < 4:
|
|
166
|
+
continue
|
|
167
|
+
code = line[:2]
|
|
168
|
+
rest = line[3:].strip()
|
|
169
|
+
if " -> " in rest:
|
|
170
|
+
rest = rest.split(" -> ", 1)[-1]
|
|
171
|
+
if rest.startswith('"') and rest.endswith('"'):
|
|
172
|
+
rest = rest[1:-1].replace('\\"', '"')
|
|
173
|
+
if code == "??":
|
|
174
|
+
paths.append(f"??{rest}")
|
|
175
|
+
elif code.strip():
|
|
176
|
+
paths.append(rest)
|
|
177
|
+
return paths
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def verify_remote_precheck_repo(
|
|
181
|
+
project_root: Path,
|
|
182
|
+
git_ref: str,
|
|
183
|
+
*,
|
|
184
|
+
checks: Tuple[str, ...],
|
|
185
|
+
skip_checks: Tuple[str, ...],
|
|
186
|
+
) -> None:
|
|
187
|
+
"""
|
|
188
|
+
Raise RemotePrecheckGitError unless origin/{git_ref} tip matches HEAD and precheck
|
|
189
|
+
input paths are clean and match that revision.
|
|
190
|
+
"""
|
|
191
|
+
repo = project_root.resolve()
|
|
192
|
+
git_marker = repo / ".git"
|
|
193
|
+
if not (git_marker.is_dir() or git_marker.is_file()):
|
|
194
|
+
raise RemotePrecheckGitError(
|
|
195
|
+
"Remote precheck requires a git checkout with .git (clone your GitHub repo, not a plain folder copy)."
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
remote_sha = _resolve_origin_tip_sha(repo, git_ref)
|
|
199
|
+
head_sha = _local_head_sha(repo)
|
|
200
|
+
if head_sha != remote_sha:
|
|
201
|
+
raise RemotePrecheckGitError(
|
|
202
|
+
f"Local HEAD ({head_sha[:7]}) must match origin {git_ref!r} ({remote_sha[:7]}). "
|
|
203
|
+
f"git checkout {git_ref} && git pull, or push your commits, then retry."
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
project_json = repo / ".cf" / "project.json"
|
|
207
|
+
critical = _critical_precheck_paths(repo, project_json, checks, skip_checks)
|
|
208
|
+
|
|
209
|
+
dirty = _porcelain_paths(repo)
|
|
210
|
+
for entry in dirty:
|
|
211
|
+
if entry.startswith("??"):
|
|
212
|
+
path = entry[2:]
|
|
213
|
+
if path in critical:
|
|
214
|
+
raise RemotePrecheckGitError(
|
|
215
|
+
f"{path!r} is untracked but required for remote precheck. "
|
|
216
|
+
"Add and commit it (or remove it) so the remote clone matches your machine."
|
|
217
|
+
)
|
|
218
|
+
elif entry in critical:
|
|
219
|
+
raise RemotePrecheckGitError(
|
|
220
|
+
f"{entry!r} has uncommitted changes. Commit or stash before remote precheck."
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
for rel in sorted(critical):
|
|
224
|
+
r = _run_git(repo, "diff-index", "--quiet", "HEAD", "--", rel)
|
|
225
|
+
if r.returncode != 0:
|
|
226
|
+
raise RemotePrecheckGitError(
|
|
227
|
+
f"{rel!r} has uncommitted changes. Commit or stash before remote precheck."
|
|
228
|
+
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|