Skip to content

Commit eb2dfcd

Browse files
Add final tapeout confirmation command and update project submission workflow in README
- Introduced `cf confirm` command to set `submission_state` to "Final" and upload project.json to SFTP. - Enhanced README with detailed instructions on confirming final tapeout and project submission states. - Updated project.json initialization to include `submission_state` defaulting to "Draft". - Implemented version auto-increment logic based on GDS hash changes in project.json.
1 parent 6079bfb commit eb2dfcd

3 files changed

Lines changed: 215 additions & 7 deletions

File tree

README.md

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,11 @@ cf --help
5858
cf view-tapeout-report
5959
```
6060

61+
8. **Confirm final tapeout** (when ready to send GDS to foundry):
62+
```bash
63+
cf confirm
64+
```
65+
6166
---
6267

6368
## Project Structure Requirements
@@ -71,6 +76,7 @@ Your project directory **must** contain:
7176
- **Note**: Both compressed (`.gz`) and uncompressed (`.gds`) files are supported
7277
- `verilog/rtl/user_defines.v` (required for digital/analog)
7378
- `.cf/project.json` (optional; will be created/updated automatically)
79+
- Contains project metadata including `submission_state` ("Draft" or "Final")
7480

7581
**Example:**
7682
```
@@ -193,6 +199,23 @@ cf pull [--project-name NAME]
193199
└── consolidated_report.html
194200
```
195201

202+
### Confirm Final Tapeout
203+
204+
```bash
205+
cf confirm [OPTIONS]
206+
```
207+
208+
- **Confirms your final tapeout** by setting `submission_state` to "Final"
209+
- **Uploads only the project.json** to the SFTP server (not the entire project)
210+
- **Use this when you're ready to send your current GDS file to the foundry** for tapeout processing
211+
- **Options:**
212+
- `--project-root`: Specify project directory
213+
- `--project-name`: Override project name
214+
- `--sftp-username`: Override configured username
215+
- `--sftp-key`: Override configured key path
216+
217+
**Important:** This command confirms that your current GDS file is ready to be sent to the foundry for tapeout. Only run this when you are completely satisfied with your design and ready for final tapeout processing. This action cannot be easily undone.
218+
196219
### View Tapeout Report
197220

198221
```bash
@@ -218,6 +241,47 @@ cf status
218241

219242
---
220243

244+
## Submission Workflow and States
245+
246+
The CLI tracks your project submission state through the `submission_state` field in `project.json`:
247+
248+
### Project States
249+
250+
- **"Draft"** - Initial state when you run `cf init`
251+
- Project is ready for development and testing
252+
- You can push updates multiple times
253+
- Project is not yet ready for tapeout processing
254+
255+
- **"Final"** - Confirmed state when you run `cf confirm`
256+
- GDS file is ready to be sent to the foundry for tapeout
257+
- No further changes should be made to the design
258+
- Only the project.json is uploaded (not the full project)
259+
260+
### Recommended Workflow
261+
262+
1. **Development Phase:**
263+
```bash
264+
cf init # Creates project with submission_state: "Draft"
265+
cf push # Upload project files (state remains "Draft")
266+
# ... make changes to your project ...
267+
cf push # Upload updated files (state remains "Draft")
268+
```
269+
270+
2. **Review Phase (Optional):**
271+
```bash
272+
cf pull # Download results for review (if available)
273+
cf view-tapeout-report # Review the tapeout report (if available)
274+
```
275+
276+
3. **Final Tapeout Confirmation:**
277+
```bash
278+
cf confirm # Confirm current GDS file is ready for foundry tapeout
279+
```
280+
281+
**Important:** Only run `cf confirm` when you are completely satisfied with your GDS file and ready to send it to the foundry for tapeout processing. This action cannot be easily undone.
282+
283+
---
284+
221285
## How the GDS Hash Works
222286

223287
- The `user_project_wrapper_hash` in `.cf/project.json` is **automatically generated and updated during `push`**
@@ -303,9 +367,15 @@ cf pull
303367
# ✓ All files downloaded to sftp-output/my_awesome_project
304368
# ✓ Project config automatically updated
305369

306-
# View the tapeout report
370+
# Review the tapeout report (if available)
307371
cf view-tapeout-report
308372
# ✓ Opened tapeout report in browser: sftp-output/my_awesome_project/consolidated_reports/consolidated_report.html
373+
374+
# When ready, confirm final tapeout (sends GDS to foundry)
375+
cf confirm
376+
# ✓ Updated project.json with submission_state = Final
377+
# ✓ Confirmed project submission: my_awesome_project
378+
# ✓ Uploaded project.json to incoming/projects/my_awesome_project/.cf/project.json
309379
```
310380

311381
### Advanced Usage
@@ -326,6 +396,9 @@ cf pull --project-name other_project
326396
# View report for specific project
327397
cf view-tapeout-report --project-name other_project
328398

399+
# Confirm final tapeout for specific project
400+
cf confirm --project-name other_project
401+
329402
# View custom report file
330403
cf view-tapeout-report --report-path /path/to/custom_report.html
331404

chipfoundry_cli/main.py

Lines changed: 114 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -196,15 +196,16 @@ def init(project_root):
196196
project_type = console.input(f"Project type (digital/analog/openframe) (detected: [cyan]{gds_type}[/cyan]): ").strip() or gds_type
197197
else:
198198
project_type = console.input("Project type (digital/analog/openframe): ").strip()
199-
version = console.input("Version (default 1.0.0): ").strip() or "1.0.0"
199+
version = "1" # Start with version 1, will be auto-incremented on push
200200
# No hash yet, will be filled by push
201201
data = {
202202
"project": {
203203
"name": name,
204204
"type": project_type,
205205
"user": username,
206206
"version": version,
207-
"user_project_wrapper_hash": ""
207+
"user_project_wrapper_hash": "",
208+
"submission_state": "Draft"
208209
}
209210
}
210211
with open(project_json_path, 'w') as f:
@@ -770,5 +771,116 @@ def view_tapeout_report(project_name, report_path):
770771
console.print(f"[red]Failed to open tapeout report in browser: {e}[/red]")
771772
raise click.Abort()
772773

774+
@main.command('confirm')
775+
@click.option('--project-root', required=False, type=click.Path(exists=True, file_okay=False), help='Path to the local ChipFoundry project directory (defaults to current directory if .cf/project.json exists).')
776+
@click.option('--sftp-host', default=DEFAULT_SFTP_HOST, show_default=True, help='SFTP server hostname.')
777+
@click.option('--sftp-username', required=False, help='SFTP username (defaults to config).')
778+
@click.option('--sftp-key', type=click.Path(exists=True, dir_okay=False), help='Path to SFTP private key file (defaults to config).', default=None, show_default=False)
779+
@click.option('--project-name', help='Project name (e.g., "my_project"). Overrides project.json if exists.')
780+
def confirm(project_root, sftp_host, sftp_username, sftp_key, project_name):
781+
"""Confirm project submission by setting submission_state to Final and pushing project.json to SFTP."""
782+
# If .cf/project.json exists in cwd, use it as default project_root and project_name
783+
cwd_root, cwd_project_name = get_project_json_from_cwd()
784+
if not project_root and cwd_root:
785+
project_root = cwd_root
786+
if not project_name and cwd_project_name:
787+
project_name = cwd_project_name
788+
if not project_root:
789+
console.print("[bold red]No project root specified and no .cf/project.json found in current directory. Please provide --project-root.[/bold red]")
790+
raise click.Abort()
791+
792+
# Load user config for defaults
793+
config = load_user_config()
794+
if not sftp_username:
795+
sftp_username = config.get("sftp_username")
796+
if not sftp_username:
797+
console.print("[bold red]No SFTP username provided and not found in config. Please run 'cf config' or provide --sftp-username.[/bold red]")
798+
raise click.Abort()
799+
if not sftp_key:
800+
sftp_key = config.get("sftp_key")
801+
802+
# Always resolve key_path to absolute path if set
803+
if sftp_key:
804+
key_path = os.path.abspath(os.path.expanduser(sftp_key))
805+
else:
806+
key_path = DEFAULT_SSH_KEY
807+
808+
if not os.path.exists(key_path):
809+
console.print(f"[red]SFTP key file not found: {key_path}[/red]")
810+
console.print("[yellow]Please run 'cf keygen' to generate a key or 'cf config' to set a custom key path.[/yellow]")
811+
raise click.Abort()
812+
813+
# Load and update project.json
814+
project_json_path = Path(project_root) / '.cf' / 'project.json'
815+
if not project_json_path.exists():
816+
console.print(f"[red]Project configuration not found at {project_json_path}[/red]")
817+
console.print("[yellow]Please run 'cf init' first to initialize your project.[/yellow]")
818+
raise click.Abort()
819+
820+
# Load existing project.json
821+
try:
822+
with open(project_json_path, 'r') as f:
823+
project_data = json.load(f)
824+
except Exception as e:
825+
console.print(f"[red]Failed to read project.json: {e}[/red]")
826+
raise click.Abort()
827+
828+
# Set submission_state to Final
829+
if "project" not in project_data:
830+
project_data["project"] = {}
831+
832+
project_data["project"]["submission_state"] = "Final"
833+
834+
# Save updated project.json
835+
try:
836+
with open(project_json_path, 'w') as f:
837+
json.dump(project_data, f, indent=2)
838+
console.print("[green]✓ Updated project.json with submission_state = Final[/green]")
839+
except Exception as e:
840+
console.print(f"[red]Failed to update project.json: {e}[/red]")
841+
raise click.Abort()
842+
843+
# Get final project name for SFTP upload
844+
final_project_name = project_name or project_data.get("project", {}).get("name")
845+
if not final_project_name:
846+
console.print("[red]No project name found in project.json. Please provide --project-name.[/red]")
847+
raise click.Abort()
848+
849+
# Connect to SFTP and upload project.json
850+
console.print(f"Connecting to {sftp_host}...")
851+
transport = None
852+
try:
853+
sftp, transport = sftp_connect(
854+
host=sftp_host,
855+
username=sftp_username,
856+
key_path=key_path
857+
)
858+
# Ensure the project directory exists before uploading
859+
sftp_project_dir = f"incoming/projects/{final_project_name}"
860+
sftp_ensure_dirs(sftp, sftp_project_dir)
861+
except Exception as e:
862+
console.print(f"[red]Failed to connect to SFTP: {e}[/red]")
863+
raise click.Abort()
864+
865+
try:
866+
# Upload only the project.json file
867+
remote_path = os.path.join(sftp_project_dir, ".cf", "project.json")
868+
upload_with_progress(
869+
sftp,
870+
local_path=str(project_json_path),
871+
remote_path=remote_path,
872+
force_overwrite=True # Always overwrite for confirmation
873+
)
874+
console.print(f"[green]✓ Confirmed project submission: {final_project_name}[/green]")
875+
console.print(f"[green]✓ Uploaded project.json to {remote_path}[/green]")
876+
877+
except Exception as e:
878+
console.print(f"[red]Upload failed: {e}[/red]")
879+
raise click.Abort()
880+
finally:
881+
if transport:
882+
sftp.close()
883+
transport.close()
884+
773885
if __name__ == "__main__":
774886
main()

chipfoundry_cli/utils.py

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ def update_or_create_project_json(
158158
) -> str:
159159
"""
160160
Update or create project.json in cf_dir. If existing_json_path is given, load and update it.
161-
Otherwise, create a new one. Always update the user_project_wrapper_hash.
161+
Otherwise, create a new one. Always update the user_project_wrapper_hash and auto-increment version.
162162
Returns the path to the updated/created project.json.
163163
"""
164164
project_json_path = str(Path(cf_dir) / "project.json")
@@ -169,14 +169,37 @@ def update_or_create_project_json(
169169
data["project"] = {}
170170
else:
171171
data = {"project": {}}
172+
173+
# Handle version auto-increment only if GDS hash changed
174+
current_version = data["project"].get("version")
175+
current_hash = data["project"].get("user_project_wrapper_hash", "")
176+
177+
if current_version is None:
178+
# No existing version, start with 1
179+
new_version = "1"
180+
elif current_hash != hash_val:
181+
# GDS hash changed, increment version
182+
try:
183+
# Convert to int, increment, and convert back to string
184+
version_num = int(current_version)
185+
new_version = str(version_num + 1)
186+
except (ValueError, TypeError):
187+
# If version is not a valid integer, start from 1
188+
new_version = "1"
189+
else:
190+
# GDS hash unchanged, keep same version
191+
new_version = current_version
192+
172193
# Required fields with defaults
173-
data["project"].setdefault("version", "1.0.0")
194+
data["project"]["version"] = new_version
174195
data["project"]["user_project_wrapper_hash"] = hash_val
175-
# Apply CLI overrides
176-
for key in ["id", "name", "type", "user", "version"]:
196+
197+
# Apply CLI overrides (but don't override auto-incremented version)
198+
for key in ["id", "name", "type", "user"]: # Removed "version" from CLI overrides
177199
cli_key = f"project_{key}" if key != "user" else "sftp_username"
178200
if cli_key in cli_overrides and cli_overrides[cli_key] is not None:
179201
data["project"][key] = cli_overrides[cli_key]
202+
180203
save_project_json(project_json_path, data)
181204
return project_json_path
182205

0 commit comments

Comments
 (0)