Skip to content

Commit 8a76eed

Browse files
Refactor precheck command and improve Docker handling
- Updated the precheck command to replace the --disable-lvs flag with --skip-checks for better flexibility in skipping specific checks. - Added a new --magic-drc flag to include optional Magic DRC checks. - Simplified Docker image handling in the setup process, ensuring clearer error messages if Docker is not found. - Enhanced test cases to reflect changes in command options and validate new functionality.
1 parent 26a8490 commit 8a76eed

2 files changed

Lines changed: 71 additions & 141 deletions

File tree

chipfoundry_cli/main.py

Lines changed: 50 additions & 128 deletions
Original file line numberDiff line numberDiff line change
@@ -2762,56 +2762,27 @@ def maybe_abort_no_space(err, step_label):
27622762
if install_precheck:
27632763
step_num = 8 if not only_mode else ""
27642764
console.print(f"\n[bold]Step {step_num}:[/bold] Installing precheck...")
2765-
precheck_dir = Path.home() / 'mpw_precheck'
27662765

2767-
# Check if already installed
2768-
is_installed = precheck_dir.exists() and (precheck_dir / '.git').exists()
2769-
2770-
if is_installed and not overwrite:
2771-
console.print("[green]✓[/green] Precheck already installed")
2766+
if shutil.which('docker') is None:
2767+
had_errors = True
2768+
console.print("[red]✗[/red] Docker not found. Install Docker Desktop to pull the precheck image.")
27722769
elif dry_run:
2773-
if is_installed:
2774-
console.print("[dim]Would reinstall mpw_precheck [--overwrite][/dim]")
2775-
else:
2776-
console.print("[dim]Would install mpw_precheck[/dim]")
2770+
console.print("[dim]Would pull chipfoundry/mpw_precheck:latest[/dim]")
27772771
else:
27782772
try:
2779-
if precheck_dir.exists() and overwrite:
2780-
console.print(f"[cyan]Removing existing {precheck_dir}...[/cyan]")
2781-
shutil.rmtree(precheck_dir)
2782-
2783-
if not precheck_dir.exists():
2784-
console.print("[cyan]Cloning mpw_precheck...[/cyan]")
2785-
subprocess.run(
2786-
['git', 'clone', '--depth=1', 'https://github.com/chipfoundry/mpw_precheck.git', str(precheck_dir)],
2787-
check=True,
2788-
capture_output=True,
2789-
text=True
2790-
)
2791-
console.print("[green]✓[/green] Precheck cloned successfully")
2792-
2793-
if shutil.which('docker') is None:
2794-
had_errors = True
2795-
console.print("[red]✗[/red] Docker not found. Install Docker Desktop to pull the precheck image.")
2796-
console.print("[dim]Precheck repo cloned, but Docker image was not pulled.[/dim]")
2797-
else:
2798-
console.print("[cyan]Pulling precheck Docker image...[/cyan]")
2799-
subprocess.run(
2800-
['docker', 'pull', 'chipfoundry/mpw_precheck:latest'],
2801-
check=True,
2802-
capture_output=True
2803-
)
2804-
console.print("[green]✓[/green] Precheck Docker image ready")
2805-
2773+
console.print("[cyan]Pulling precheck Docker image...[/cyan]")
2774+
subprocess.run(
2775+
['docker', 'pull', 'chipfoundry/mpw_precheck:latest'],
2776+
check=True,
2777+
capture_output=True
2778+
)
2779+
console.print("[green]✓[/green] Precheck Docker image ready")
28062780
except subprocess.CalledProcessError as e:
28072781
maybe_abort_no_space(e, "Precheck install")
28082782
had_errors = True
2809-
console.print(f"[red]✗[/red] Failed to install precheck: {e}")
2783+
console.print(f"[red]✗[/red] Failed to pull precheck image: {e}")
28102784
if e.stderr:
28112785
console.print(f"[dim]{e.stderr}[/dim]")
2812-
except OSError as e:
2813-
had_errors = True
2814-
console.print(f"[red]✗[/red] Failed to install precheck: {e}")
28152786

28162787
# Summary
28172788
console.print("\n" + "="*60)
@@ -3216,21 +3187,22 @@ def repo_update(project_root, repo_owner, repo_name, branch, dry_run):
32163187

32173188
@main.command('precheck')
32183189
@click.option('--project-root', type=click.Path(exists=True, file_okay=False), help='Path to the project directory (defaults to current directory)')
3219-
@click.option('--disable-lvs', is_flag=True, help='Disable LVS check and run specific checks only')
3190+
@click.option('--skip-checks', multiple=True, help='Checks to skip (can be specified multiple times)')
3191+
@click.option('--magic-drc', is_flag=True, help='Include Magic DRC check (optional, off by default)')
32203192
@click.option('--checks', multiple=True, help='Specific checks to run (can be specified multiple times)')
32213193
@click.option('--dry-run', is_flag=True, help='Show the command without running')
3222-
def precheck(project_root, disable_lvs, checks, dry_run):
3223-
"""Run mpw_precheck validation on the project.
3194+
def precheck(project_root, skip_checks, magic_drc, checks, dry_run):
3195+
"""Run precheck validation on the project.
32243196
3225-
This runs the MPW (Multi-Project Wafer) precheck tool to validate
3226-
your design before submission.
3197+
This runs the cf-precheck tool to validate your design before
3198+
submission.
32273199
32283200
Examples:
3229-
cf precheck # Run all checks
3230-
cf precheck --disable-lvs # Skip LVS, run specific checks
3231-
cf precheck --checks license --checks makefile # Run specific checks
3201+
cf precheck # Run all checks
3202+
cf precheck --skip-checks lvs # Skip LVS check
3203+
cf precheck --magic-drc # Include optional Magic DRC
3204+
cf precheck --checks topcell_check # Run specific checks only
32323205
"""
3233-
# If .cf/project.json exists in cwd, use it as default project_root
32343206
cwd_root, _ = get_project_json_from_cwd()
32353207
if not project_root and cwd_root:
32363208
project_root = cwd_root
@@ -3239,20 +3211,17 @@ def precheck(project_root, disable_lvs, checks, dry_run):
32393211

32403212
project_root_path = Path(project_root)
32413213

3242-
# Check if project is initialized (allow graceful return)
32433214
if not check_project_initialized(project_root_path, 'precheck', dry_run=dry_run, allow_graceful=True):
32443215
console.print(f"[red]✗[/red] Project not initialized. Please run 'cf init' first.")
32453216
console.print("[yellow]Dependencies are required before running precheck.[/yellow]")
32463217
return
32473218

32483219
project_json_path = project_root_path / '.cf' / 'project.json'
32493220

3250-
# Check project type - GPIO config not needed for openframe
32513221
with open(project_json_path, 'r') as f:
32523222
project_data = json.load(f)
32533223
project_type = project_data.get('project', {}).get('type', 'digital')
32543224

3255-
# Check if GPIO configuration exists (not needed for openframe)
32563225
if project_type != 'openframe':
32573226
gpio_config = get_gpio_config_from_project_json(str(project_json_path))
32583227
if not gpio_config or len(gpio_config) == 0:
@@ -3261,10 +3230,8 @@ def precheck(project_root, disable_lvs, checks, dry_run):
32613230
console.print("[cyan]Please run 'cf gpio-config' to configure GPIO settings first.[/cyan]")
32623231
raise click.Abort()
32633232

3264-
precheck_root = Path.home() / 'mpw_precheck'
32653233
pdk_root = project_root_path / 'dependencies' / 'pdks'
32663234

3267-
# Detect PDK from project.json
32683235
pdk = 'sky130A'
32693236
if project_json_path.exists():
32703237
try:
@@ -3274,124 +3241,79 @@ def precheck(project_root, disable_lvs, checks, dry_run):
32743241
except:
32753242
pass
32763243

3277-
# Check if precheck is installed
3278-
if not precheck_root.exists():
3279-
console.print(f"[red]✗[/red] mpw_precheck not found at {precheck_root}")
3280-
console.print("[yellow]Run 'cf setup --only-precheck' to install[/yellow]")
3281-
return
3282-
3283-
# Check if PDK exists
32843244
if not (pdk_root / pdk).exists():
32853245
console.print(f"[red]✗[/red] PDK not found at {pdk_root / pdk}")
32863246
console.print("[yellow]Run 'cf setup --only-pdk' to install[/yellow]")
32873247
return
32883248

3289-
# Check Docker availability
3290-
docker_available = shutil.which('docker') is not None
3291-
if not docker_available:
3249+
if shutil.which('docker') is None:
32923250
console.print("[red]✗[/red] Docker not found. Docker is required to run precheck.")
32933251
return
32943252

3295-
# Build the checks list
3296-
if checks:
3297-
# User specified custom checks
3298-
checks_list = list(checks)
3299-
elif disable_lvs:
3300-
# Default checks when LVS is disabled
3301-
checks_list = [
3302-
'license', 'makefile', 'default', 'documentation', 'consistency',
3303-
'gpio_defines', 'xor', 'magic_drc', 'klayout_feol', 'klayout_beol',
3304-
'klayout_offgrid', 'klayout_met_min_ca_density',
3305-
'klayout_pin_label_purposes_overlapping_drawing', 'klayout_zeroarea'
3306-
]
3307-
else:
3308-
# All checks (default behavior)
3309-
checks_list = []
3310-
3311-
# Display configuration
3312-
console.print("\n" + "="*60)
3313-
console.print("[bold cyan]MPW Precheck[/bold cyan]")
3314-
console.print(f"Project: [yellow]{project_root_path}[/yellow]")
3315-
console.print(f"PDK: [yellow]{pdk}[/yellow]")
3316-
if disable_lvs:
3317-
console.print("Mode: [yellow]LVS disabled[/yellow]")
3318-
if checks_list:
3319-
console.print(f"Checks: [yellow]{', '.join(checks_list)}[/yellow]")
3320-
else:
3321-
console.print("Checks: [yellow]All checks[/yellow]")
3322-
console.print("="*60 + "\n")
3323-
3324-
# Build Docker command
3325-
import getpass
3326-
import pwd
3327-
3253+
pdk_path = pdk_root / pdk
33283254
user_id = os.getuid()
33293255
group_id = os.getgid()
33303256

3331-
pdk_path = pdk_root / pdk
3332-
pdkpath = pdk_path # Same as PDK_PATH in the Makefile
3333-
ipm_dir = Path.home() / '.ipm'
3334-
3335-
# Create .ipm directory if it doesn't exist
3336-
if not ipm_dir.exists():
3337-
ipm_dir.mkdir(parents=True, exist_ok=True)
3338-
33393257
docker_cmd = [
33403258
'docker', 'run', '--rm',
3341-
'-v', f'{precheck_root}:{precheck_root}',
33423259
'-v', f'{project_root_path}:{project_root_path}',
33433260
'-v', f'{pdk_root}:{pdk_root}',
3344-
'-v', f'{ipm_dir}:{ipm_dir}',
3345-
'-e', f'INPUT_DIRECTORY={project_root_path}',
3346-
'-e', f'PDK_PATH={pdk_path}',
3347-
'-e', f'PDK_ROOT={pdk_root}',
3348-
'-e', f'PDKPATH={pdkpath}',
33493261
'-u', f'{user_id}:{group_id}',
33503262
'chipfoundry/mpw_precheck:latest',
3351-
'bash', '-c',
3263+
'cf-precheck',
3264+
'-i', str(project_root_path),
3265+
'-p', str(pdk_path),
3266+
'-c', '/opt/caravel',
33523267
]
33533268

3354-
# Build the precheck command
3355-
precheck_cmd = f'cd {precheck_root} ; python3 mpw_precheck.py --input_directory {project_root_path} --pdk_path {pdk_path}'
3269+
if magic_drc:
3270+
docker_cmd.append('--magic-drc')
33563271

3357-
if checks_list:
3358-
precheck_cmd += ' ' + ' '.join(checks_list)
3272+
if skip_checks:
3273+
docker_cmd.extend(['--skip-checks'] + list(skip_checks))
33593274

3360-
docker_cmd.append(precheck_cmd)
3275+
if checks:
3276+
docker_cmd.extend(list(checks))
3277+
3278+
checks_display = ', '.join(checks) if checks else 'All checks'
3279+
console.print("\n" + "="*60)
3280+
console.print("[bold cyan]CF Precheck[/bold cyan]")
3281+
console.print(f"Project: [yellow]{project_root_path}[/yellow]")
3282+
console.print(f"PDK: [yellow]{pdk}[/yellow]")
3283+
if skip_checks:
3284+
console.print(f"Skipping: [yellow]{', '.join(skip_checks)}[/yellow]")
3285+
if magic_drc:
3286+
console.print("Magic DRC: [yellow]enabled[/yellow]")
3287+
console.print(f"Checks: [yellow]{checks_display}[/yellow]")
3288+
console.print("="*60 + "\n")
33613289

33623290
if dry_run:
33633291
console.print("[bold yellow]Dry run - would execute:[/bold yellow]\n")
33643292
console.print("[dim]" + ' '.join(docker_cmd) + "[/dim]")
33653293
return
33663294

3367-
# Run precheck
3368-
console.print("[cyan]Running mpw_precheck...[/cyan]")
3295+
console.print("[cyan]Running cf-precheck...[/cyan]\n")
33693296

33703297
try:
3371-
# Use Popen for better signal handling
33723298
process = subprocess.Popen(
33733299
docker_cmd,
3374-
cwd=str(precheck_root),
33753300
preexec_fn=os.setsid if os.name != 'nt' else None
33763301
)
33773302

3378-
# Wait for process to complete
33793303
returncode = process.wait()
33803304

3381-
console.print("") # Add newline
3305+
console.print("")
33823306
if returncode == 0:
33833307
console.print("[green]✓[/green] Precheck passed!")
3384-
elif returncode == -2 or returncode == 130: # SIGINT
3308+
elif returncode == -2 or returncode == 130:
33853309
console.print("[yellow]⚠[/yellow] Precheck interrupted by user")
33863310
sys.exit(130)
33873311
else:
3388-
console.print(f"[red]✗[/red] Precheck failed with exit code {returncode}")
3389-
console.print(f"[yellow]Check the output above for details[/yellow]")
3312+
console.print(f"[red]✗[/red] Precheck failed (exit code {returncode})")
33903313
sys.exit(returncode)
33913314

33923315
except KeyboardInterrupt:
33933316
console.print("\n[yellow]⚠[/yellow] Precheck interrupted by user")
3394-
# Try to stop the Docker container gracefully
33953317
try:
33963318
if os.name != 'nt':
33973319
os.killpg(os.getpgid(process.pid), signal.SIGTERM)

tests/test_precheck_command.py

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,10 @@ def test_precheck_help(self):
2929
result = runner.invoke(main, ['precheck', '--help'])
3030

3131
assert result.exit_code == 0
32-
assert 'Run mpw_precheck validation' in result.output
32+
assert 'Run precheck validation' in result.output
3333
assert '--project-root' in result.output
34-
assert '--disable-lvs' in result.output
34+
assert '--skip-checks' in result.output
35+
assert '--magic-drc' in result.output
3536
assert '--checks' in result.output
3637
assert '--dry-run' in result.output
3738

@@ -44,24 +45,20 @@ def test_precheck_dry_run(self, temp_project_dir):
4445
'--dry-run'
4546
])
4647

47-
# Command returns 0 even on error, just prints error message
4848
assert result.exit_code == 0
49-
# May mention precheck, pdk, or setup in error message
5049
assert any(keyword in result.output.lower() for keyword in ['precheck', 'pdk', 'setup', 'dry'])
5150

52-
def test_precheck_disable_lvs(self, temp_project_dir):
53-
"""Test precheck command with --disable-lvs flag."""
51+
def test_precheck_skip_checks(self, temp_project_dir):
52+
"""Test precheck command with --skip-checks flag."""
5453
runner = CliRunner()
5554
result = runner.invoke(main, [
5655
'precheck',
5756
'--project-root', temp_project_dir,
58-
'--disable-lvs',
57+
'--skip-checks', 'lvs',
5958
'--dry-run'
6059
])
6160

62-
# Command returns 0 even on error, just prints error message
6361
assert result.exit_code == 0
64-
# May mention precheck, pdk, or setup in error message
6562
assert any(keyword in result.output.lower() for keyword in ['precheck', 'pdk', 'setup', 'dry'])
6663

6764
def test_precheck_with_checks(self, temp_project_dir):
@@ -70,14 +67,25 @@ def test_precheck_with_checks(self, temp_project_dir):
7067
result = runner.invoke(main, [
7168
'precheck',
7269
'--project-root', temp_project_dir,
73-
'--checks', 'license',
74-
'--checks', 'makefile',
70+
'--checks', 'topcell_check',
71+
'--checks', 'gpio_defines',
72+
'--dry-run'
73+
])
74+
75+
assert result.exit_code == 0
76+
assert any(keyword in result.output.lower() for keyword in ['precheck', 'pdk', 'setup', 'dry'])
77+
78+
def test_precheck_magic_drc(self, temp_project_dir):
79+
"""Test precheck command with --magic-drc flag."""
80+
runner = CliRunner()
81+
result = runner.invoke(main, [
82+
'precheck',
83+
'--project-root', temp_project_dir,
84+
'--magic-drc',
7585
'--dry-run'
7686
])
7787

78-
# Command returns 0 even on error, just prints error message
7988
assert result.exit_code == 0
80-
# May mention precheck, pdk, or setup in error message
8189
assert any(keyword in result.output.lower() for keyword in ['precheck', 'pdk', 'setup', 'dry'])
8290

8391

0 commit comments

Comments
 (0)