chipfoundry-cli 0.1.1__tar.gz → 0.1.2__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-0.1.1 → chipfoundry_cli-0.1.2}/PKG-INFO +35 -14
- {chipfoundry_cli-0.1.1 → chipfoundry_cli-0.1.2}/README.md +33 -13
- chipfoundry_cli-0.1.2/chipfoundry_cli/main.py +454 -0
- {chipfoundry_cli-0.1.1 → chipfoundry_cli-0.1.2}/chipfoundry_cli/utils.py +17 -1
- {chipfoundry_cli-0.1.1 → chipfoundry_cli-0.1.2}/pyproject.toml +3 -1
- chipfoundry_cli-0.1.1/chipfoundry_cli/main.py +0 -173
- {chipfoundry_cli-0.1.1 → chipfoundry_cli-0.1.2}/LICENSE +0 -0
- {chipfoundry_cli-0.1.1 → chipfoundry_cli-0.1.2}/chipfoundry_cli/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: chipfoundry-cli
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: CLI tool to automate ChipFoundry project submission to SFTP server
|
|
5
5
|
Home-page: https://chipfoundry.io
|
|
6
6
|
License: Apache-2.0
|
|
@@ -18,6 +18,7 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
18
18
|
Requires-Dist: click (>=8.0.0,<9)
|
|
19
19
|
Requires-Dist: paramiko (>=3.0.0,<4)
|
|
20
20
|
Requires-Dist: rich (>=13,<14)
|
|
21
|
+
Requires-Dist: toml (>=0.10,<1.0)
|
|
21
22
|
Project-URL: Repository, https://github.com/chipfoundry/cf-cli
|
|
22
23
|
Description-Content-Type: text/markdown
|
|
23
24
|
|
|
@@ -38,8 +39,8 @@ A command-line tool to automate the submission of ChipFoundry projects to the SF
|
|
|
38
39
|
Install from PyPI:
|
|
39
40
|
|
|
40
41
|
```bash
|
|
41
|
-
pip install
|
|
42
|
-
|
|
42
|
+
pip install chipfoundry-cli
|
|
43
|
+
chipfoundry --help
|
|
43
44
|
```
|
|
44
45
|
|
|
45
46
|
---
|
|
@@ -86,39 +87,57 @@ my_project/
|
|
|
86
87
|
|
|
87
88
|
## Usage
|
|
88
89
|
|
|
89
|
-
###
|
|
90
|
+
### Configure User Credentials
|
|
90
91
|
|
|
91
92
|
```bash
|
|
92
|
-
|
|
93
|
+
chipfoundry config
|
|
93
94
|
```
|
|
95
|
+
- Prompts for your SFTP username and key path. Only needs to be run once per user/machine.
|
|
94
96
|
|
|
95
|
-
###
|
|
97
|
+
### Initialize a New Project
|
|
96
98
|
|
|
97
99
|
```bash
|
|
98
|
-
|
|
100
|
+
chipfoundry init
|
|
99
101
|
```
|
|
102
|
+
- Prompts for project name, type (auto-detected from GDS file if present), and version.
|
|
103
|
+
- Creates `.cf/project.json` in the current directory.
|
|
104
|
+
- **Note:** The GDS hash is NOT generated at this step (see below).
|
|
100
105
|
|
|
101
|
-
###
|
|
106
|
+
### Push a Project (Upload)
|
|
102
107
|
|
|
103
108
|
```bash
|
|
104
|
-
|
|
109
|
+
chipfoundry push
|
|
105
110
|
```
|
|
111
|
+
- Run from your project directory (with `.cf/project.json`).
|
|
112
|
+
- Collects files, updates the GDS hash, and uploads to SFTP.
|
|
106
113
|
|
|
107
|
-
###
|
|
114
|
+
### Pull Results
|
|
108
115
|
|
|
109
116
|
```bash
|
|
110
|
-
|
|
117
|
+
chipfoundry pull
|
|
111
118
|
```
|
|
119
|
+
- Downloads results for the current project to a local directory.
|
|
112
120
|
|
|
113
|
-
###
|
|
121
|
+
### Check Status
|
|
114
122
|
|
|
115
123
|
```bash
|
|
116
|
-
|
|
124
|
+
chipfoundry status
|
|
117
125
|
```
|
|
126
|
+
- Shows all your projects and their input/output status on the SFTP server.
|
|
118
127
|
|
|
119
128
|
---
|
|
120
129
|
|
|
121
|
-
##
|
|
130
|
+
## How the GDS Hash Works
|
|
131
|
+
|
|
132
|
+
- The `user_project_wrapper_hash` in `.cf/project.json` is **automatically generated and updated during `push`**.
|
|
133
|
+
- The hash is calculated from the actual GDS file being uploaded.
|
|
134
|
+
- This ensures the hash always matches the file you are submitting.
|
|
135
|
+
- **You do not need to manage or update the hash manually.**
|
|
136
|
+
- The hash is NOT generated during `init` because the GDS file may not exist or may change before submission.
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
## What Happens When You Run `chipfoundry push`?
|
|
122
141
|
|
|
123
142
|
1. **File Collection:**
|
|
124
143
|
- The tool checks for the required GDS and Verilog files.
|
|
@@ -146,6 +165,8 @@ cf submit --project-root /path/to/my_project --sftp-username <your_chipfoundry_u
|
|
|
146
165
|
- Check your network connection and credentials.
|
|
147
166
|
- **Project type detection:**
|
|
148
167
|
- Only one of the recognized GDS files should be present in your `gds/` directory.
|
|
168
|
+
- **ModuleNotFoundError: No module named 'toml':**
|
|
169
|
+
- This means your environment is missing the `toml` dependency. Upgrade `chipfoundry-cli` with `pip install --upgrade chipfoundry-cli`, or install `toml` manually with `pip install 'toml>=0.10,<1.0'`.
|
|
149
170
|
|
|
150
171
|
---
|
|
151
172
|
|
|
@@ -15,8 +15,8 @@ A command-line tool to automate the submission of ChipFoundry projects to the SF
|
|
|
15
15
|
Install from PyPI:
|
|
16
16
|
|
|
17
17
|
```bash
|
|
18
|
-
pip install
|
|
19
|
-
|
|
18
|
+
pip install chipfoundry-cli
|
|
19
|
+
chipfoundry --help
|
|
20
20
|
```
|
|
21
21
|
|
|
22
22
|
---
|
|
@@ -63,39 +63,57 @@ my_project/
|
|
|
63
63
|
|
|
64
64
|
## Usage
|
|
65
65
|
|
|
66
|
-
###
|
|
66
|
+
### Configure User Credentials
|
|
67
67
|
|
|
68
68
|
```bash
|
|
69
|
-
|
|
69
|
+
chipfoundry config
|
|
70
70
|
```
|
|
71
|
+
- Prompts for your SFTP username and key path. Only needs to be run once per user/machine.
|
|
71
72
|
|
|
72
|
-
###
|
|
73
|
+
### Initialize a New Project
|
|
73
74
|
|
|
74
75
|
```bash
|
|
75
|
-
|
|
76
|
+
chipfoundry init
|
|
76
77
|
```
|
|
78
|
+
- Prompts for project name, type (auto-detected from GDS file if present), and version.
|
|
79
|
+
- Creates `.cf/project.json` in the current directory.
|
|
80
|
+
- **Note:** The GDS hash is NOT generated at this step (see below).
|
|
77
81
|
|
|
78
|
-
###
|
|
82
|
+
### Push a Project (Upload)
|
|
79
83
|
|
|
80
84
|
```bash
|
|
81
|
-
|
|
85
|
+
chipfoundry push
|
|
82
86
|
```
|
|
87
|
+
- Run from your project directory (with `.cf/project.json`).
|
|
88
|
+
- Collects files, updates the GDS hash, and uploads to SFTP.
|
|
83
89
|
|
|
84
|
-
###
|
|
90
|
+
### Pull Results
|
|
85
91
|
|
|
86
92
|
```bash
|
|
87
|
-
|
|
93
|
+
chipfoundry pull
|
|
88
94
|
```
|
|
95
|
+
- Downloads results for the current project to a local directory.
|
|
89
96
|
|
|
90
|
-
###
|
|
97
|
+
### Check Status
|
|
91
98
|
|
|
92
99
|
```bash
|
|
93
|
-
|
|
100
|
+
chipfoundry status
|
|
94
101
|
```
|
|
102
|
+
- Shows all your projects and their input/output status on the SFTP server.
|
|
95
103
|
|
|
96
104
|
---
|
|
97
105
|
|
|
98
|
-
##
|
|
106
|
+
## How the GDS Hash Works
|
|
107
|
+
|
|
108
|
+
- The `user_project_wrapper_hash` in `.cf/project.json` is **automatically generated and updated during `push`**.
|
|
109
|
+
- The hash is calculated from the actual GDS file being uploaded.
|
|
110
|
+
- This ensures the hash always matches the file you are submitting.
|
|
111
|
+
- **You do not need to manage or update the hash manually.**
|
|
112
|
+
- The hash is NOT generated during `init` because the GDS file may not exist or may change before submission.
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## What Happens When You Run `chipfoundry push`?
|
|
99
117
|
|
|
100
118
|
1. **File Collection:**
|
|
101
119
|
- The tool checks for the required GDS and Verilog files.
|
|
@@ -123,6 +141,8 @@ cf submit --project-root /path/to/my_project --sftp-username <your_chipfoundry_u
|
|
|
123
141
|
- Check your network connection and credentials.
|
|
124
142
|
- **Project type detection:**
|
|
125
143
|
- Only one of the recognized GDS files should be present in your `gds/` directory.
|
|
144
|
+
- **ModuleNotFoundError: No module named 'toml':**
|
|
145
|
+
- This means your environment is missing the `toml` dependency. Upgrade `chipfoundry-cli` with `pip install --upgrade chipfoundry-cli`, or install `toml` manually with `pip install 'toml>=0.10,<1.0'`.
|
|
126
146
|
|
|
127
147
|
---
|
|
128
148
|
|
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
import click
|
|
2
|
+
import getpass
|
|
3
|
+
from chipfoundry_cli.utils import (
|
|
4
|
+
collect_project_files, ensure_cf_directory, update_or_create_project_json,
|
|
5
|
+
sftp_connect, upload_with_progress, sftp_ensure_dirs,
|
|
6
|
+
get_config_path, load_user_config, save_user_config
|
|
7
|
+
)
|
|
8
|
+
import os
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from rich.console import Console
|
|
11
|
+
from rich.panel import Panel
|
|
12
|
+
from rich.text import Text
|
|
13
|
+
import importlib.metadata
|
|
14
|
+
from rich.table import Table
|
|
15
|
+
from rich.progress import Progress, BarColumn, TextColumn, TimeElapsedColumn, TaskProgressColumn
|
|
16
|
+
import json
|
|
17
|
+
|
|
18
|
+
DEFAULT_SSH_KEY = os.path.expanduser('~/.ssh/id_rsa')
|
|
19
|
+
DEFAULT_SFTP_HOST = 'sftp.chipfoundry.io'
|
|
20
|
+
|
|
21
|
+
GDS_TYPE_MAP = {
|
|
22
|
+
'user_project_wrapper.gds': 'digital',
|
|
23
|
+
'user_analog_project_wrapper.gds': 'analog',
|
|
24
|
+
'openframe_project_wrapper.gds': 'openframe',
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
console = Console()
|
|
28
|
+
|
|
29
|
+
def get_project_json_from_cwd():
|
|
30
|
+
cf_path = Path(os.getcwd()) / '.cf' / 'project.json'
|
|
31
|
+
if cf_path.exists():
|
|
32
|
+
with open(cf_path) as f:
|
|
33
|
+
data = json.load(f)
|
|
34
|
+
project_name = data.get('project', {}).get('name')
|
|
35
|
+
return str(Path(os.getcwd())), project_name
|
|
36
|
+
return None, None
|
|
37
|
+
|
|
38
|
+
@click.group(help="ChipFoundry CLI: Automate project submission and management.")
|
|
39
|
+
@click.version_option(importlib.metadata.version("chipfoundry-cli"), "-v", "--version", message="%(version)s")
|
|
40
|
+
def main():
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
@main.command('config')
|
|
44
|
+
def config_cmd():
|
|
45
|
+
"""Configure user-level SFTP credentials (username and key)."""
|
|
46
|
+
console.print("[bold cyan]ChipFoundry CLI User Configuration[/bold cyan]")
|
|
47
|
+
username = console.input("Enter your ChipFoundry SFTP username: ").strip()
|
|
48
|
+
key_path = console.input("Enter path to your SFTP private key (leave blank for ~/.ssh/id_rsa): ").strip()
|
|
49
|
+
if not key_path:
|
|
50
|
+
key_path = os.path.expanduser('~/.ssh/id_rsa')
|
|
51
|
+
config = {
|
|
52
|
+
"sftp_username": username,
|
|
53
|
+
"sftp_key": key_path,
|
|
54
|
+
}
|
|
55
|
+
save_user_config(config)
|
|
56
|
+
console.print(f"[green]Configuration saved to {get_config_path()}[/green]")
|
|
57
|
+
|
|
58
|
+
@main.command('init')
|
|
59
|
+
@click.option('--project-root', required=False, type=click.Path(file_okay=False), help='Directory to create the project in (defaults to current directory).')
|
|
60
|
+
def init(project_root):
|
|
61
|
+
"""Initialize a new ChipFoundry project (.cf/project.json) in the given directory."""
|
|
62
|
+
if not project_root:
|
|
63
|
+
project_root = os.getcwd()
|
|
64
|
+
cf_dir = Path(project_root) / '.cf'
|
|
65
|
+
cf_dir.mkdir(parents=True, exist_ok=True)
|
|
66
|
+
project_json_path = cf_dir / 'project.json'
|
|
67
|
+
if project_json_path.exists():
|
|
68
|
+
overwrite = console.input(f"[yellow]project.json already exists at {project_json_path}. Overwrite? (y/N): [/yellow]").strip().lower()
|
|
69
|
+
if overwrite != 'y':
|
|
70
|
+
console.print("[red]Aborted project initialization.[/red]")
|
|
71
|
+
return
|
|
72
|
+
# Get username from user config
|
|
73
|
+
config = load_user_config()
|
|
74
|
+
username = config.get("sftp_username")
|
|
75
|
+
if not username:
|
|
76
|
+
console.print("[bold red]No SFTP username found in user config. Please run 'chipfoundry config' first.[/bold red]")
|
|
77
|
+
raise click.Abort()
|
|
78
|
+
# Auto-detect project type from GDS file name
|
|
79
|
+
gds_dir = Path(project_root) / 'gds'
|
|
80
|
+
gds_type = None
|
|
81
|
+
gds_type_map = {
|
|
82
|
+
'user_project_wrapper.gds': 'digital',
|
|
83
|
+
'user_analog_project_wrapper.gds': 'analog',
|
|
84
|
+
'openframe_project_wrapper.gds': 'openframe',
|
|
85
|
+
}
|
|
86
|
+
for gds_name, gtype in gds_type_map.items():
|
|
87
|
+
if (gds_dir / gds_name).exists():
|
|
88
|
+
gds_type = gtype
|
|
89
|
+
break
|
|
90
|
+
name = console.input("Project name: ").strip()
|
|
91
|
+
# Suggest project type if detected
|
|
92
|
+
if gds_type:
|
|
93
|
+
project_type = console.input(f"Project type (digital/analog/openframe) [default: {gds_type}]: ").strip() or gds_type
|
|
94
|
+
else:
|
|
95
|
+
project_type = console.input("Project type (digital/analog/openframe): ").strip()
|
|
96
|
+
version = console.input("Version (default 1.0.0): ").strip() or "1.0.0"
|
|
97
|
+
# No hash yet, will be filled by push
|
|
98
|
+
data = {
|
|
99
|
+
"project": {
|
|
100
|
+
"name": name,
|
|
101
|
+
"type": project_type,
|
|
102
|
+
"user": username,
|
|
103
|
+
"version": version,
|
|
104
|
+
"user_project_wrapper_hash": ""
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
with open(project_json_path, 'w') as f:
|
|
108
|
+
json.dump(data, f, indent=2)
|
|
109
|
+
console.print(f"[green]Initialized project at {project_json_path}[/green]")
|
|
110
|
+
|
|
111
|
+
@main.command('push')
|
|
112
|
+
@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).')
|
|
113
|
+
@click.option('--sftp-host', default=DEFAULT_SFTP_HOST, show_default=True, help='SFTP server hostname.')
|
|
114
|
+
@click.option('--sftp-username', required=False, help='SFTP username (defaults to config).')
|
|
115
|
+
@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)
|
|
116
|
+
@click.option('--sftp-password', help='SFTP password. If not provided, will prompt securely.', default=None)
|
|
117
|
+
@click.option('--project-id', help='Project ID (e.g., "user123_proj456"). Overrides project.json if exists.')
|
|
118
|
+
@click.option('--project-name', help='Project name (e.g., "my_project"). Overrides project.json if exists.')
|
|
119
|
+
@click.option('--project-type', help='Project type (auto-detected if not provided).', default=None)
|
|
120
|
+
@click.option('--force-overwrite', is_flag=True, help='Overwrite existing files on SFTP without prompting.')
|
|
121
|
+
@click.option('--dry-run', is_flag=True, help='Preview actions without uploading files.')
|
|
122
|
+
def push(project_root, sftp_host, sftp_username, sftp_key, sftp_password, project_id, project_name, project_type, force_overwrite, dry_run):
|
|
123
|
+
"""Upload your project files to the ChipFoundry SFTP server."""
|
|
124
|
+
# If .cf/project.json exists in cwd, use it as default project_root and project_name
|
|
125
|
+
cwd_root, cwd_project_name = get_project_json_from_cwd()
|
|
126
|
+
if not project_root and cwd_root:
|
|
127
|
+
project_root = cwd_root
|
|
128
|
+
if not project_name and cwd_project_name:
|
|
129
|
+
project_name = cwd_project_name
|
|
130
|
+
if not project_root:
|
|
131
|
+
console.print("[bold red]No project root specified and no .cf/project.json found in current directory. Please provide --project-root.[/bold red]")
|
|
132
|
+
raise click.Abort()
|
|
133
|
+
# Load user config for defaults
|
|
134
|
+
config = load_user_config()
|
|
135
|
+
if not sftp_username:
|
|
136
|
+
sftp_username = config.get("sftp_username")
|
|
137
|
+
if not sftp_username:
|
|
138
|
+
console.print("[bold red]No SFTP username provided and not found in config. Please run 'chipfoundry init' or provide --sftp-username.[/bold red]")
|
|
139
|
+
raise click.Abort()
|
|
140
|
+
if not sftp_key:
|
|
141
|
+
sftp_key = config.get("sftp_key")
|
|
142
|
+
# Determine which authentication method to use
|
|
143
|
+
key_path = sftp_key
|
|
144
|
+
password = sftp_password
|
|
145
|
+
if not key_path and not password:
|
|
146
|
+
if os.path.exists(DEFAULT_SSH_KEY):
|
|
147
|
+
key_path = DEFAULT_SSH_KEY
|
|
148
|
+
console.print(f"[INFO] Using default SSH key: {DEFAULT_SSH_KEY}", style="bold cyan")
|
|
149
|
+
else:
|
|
150
|
+
console.print("[WARN] No SFTP key or password provided, and no default key found at ~/.ssh/id_rsa.", style="bold yellow")
|
|
151
|
+
auth_method = click.prompt("Choose authentication method (key/password)", type=click.Choice(['key', 'password']), show_choices=True)
|
|
152
|
+
if auth_method == 'key':
|
|
153
|
+
key_path = click.prompt("Enter path to SFTP private key", type=click.Path(exists=True, dir_okay=False))
|
|
154
|
+
else:
|
|
155
|
+
password = click.prompt("SFTP Password", hide_input=True)
|
|
156
|
+
elif key_path and password:
|
|
157
|
+
console.print("[ERROR] Options --sftp-password and --sftp-key are mutually exclusive.", style="bold red")
|
|
158
|
+
raise click.UsageError("Options --sftp-password and --sftp-key are mutually exclusive.")
|
|
159
|
+
elif not key_path and password:
|
|
160
|
+
pass # password provided
|
|
161
|
+
elif key_path and not password:
|
|
162
|
+
if not os.path.exists(key_path):
|
|
163
|
+
console.print(f"[ERROR] SFTP key file not found: {key_path}", style="bold red")
|
|
164
|
+
raise click.UsageError(f"SFTP key file not found: {key_path}")
|
|
165
|
+
|
|
166
|
+
console.print(f"[INFO] Collecting project files from: {project_root}", style="bold cyan")
|
|
167
|
+
try:
|
|
168
|
+
collected = collect_project_files(project_root)
|
|
169
|
+
for rel_path, abs_path in collected.items():
|
|
170
|
+
if abs_path:
|
|
171
|
+
console.print(f"[OK] Found: {rel_path} -> {abs_path}", style="green")
|
|
172
|
+
else:
|
|
173
|
+
console.print(f"[INFO] Optional file not found: {rel_path}", style="yellow")
|
|
174
|
+
except FileNotFoundError as e:
|
|
175
|
+
console.print(f"[ERROR] {e}", style="bold red")
|
|
176
|
+
raise click.Abort()
|
|
177
|
+
|
|
178
|
+
# Auto-detect project type from GDS file name if not provided
|
|
179
|
+
gds_dir = Path(project_root) / 'gds'
|
|
180
|
+
found_types = []
|
|
181
|
+
gds_file_path = None
|
|
182
|
+
for gds_name, gds_type in GDS_TYPE_MAP.items():
|
|
183
|
+
candidate = gds_dir / gds_name
|
|
184
|
+
if candidate.exists():
|
|
185
|
+
found_types.append(gds_type)
|
|
186
|
+
gds_file_path = str(candidate)
|
|
187
|
+
if project_type:
|
|
188
|
+
detected_type = project_type
|
|
189
|
+
else:
|
|
190
|
+
if len(found_types) == 0:
|
|
191
|
+
console.print("[ERROR] No recognized GDS file found for project type detection.", style="bold red")
|
|
192
|
+
raise click.Abort()
|
|
193
|
+
elif len(found_types) > 1:
|
|
194
|
+
console.print(f"[ERROR] Multiple GDS types found: {found_types}. Only one project type is allowed per project.", style="bold red")
|
|
195
|
+
raise click.Abort()
|
|
196
|
+
else:
|
|
197
|
+
detected_type = found_types[0]
|
|
198
|
+
console.print(f"[INFO] Detected project type: {detected_type}", style="bold cyan")
|
|
199
|
+
# Use the detected GDS file for upload and hash
|
|
200
|
+
if gds_file_path:
|
|
201
|
+
collected['gds/user_project_wrapper.gds'] = gds_file_path
|
|
202
|
+
# Prepare CLI overrides for project.json
|
|
203
|
+
cli_overrides = {
|
|
204
|
+
"project_id": project_id,
|
|
205
|
+
"project_name": project_name,
|
|
206
|
+
"project_type": detected_type,
|
|
207
|
+
"sftp_username": sftp_username,
|
|
208
|
+
}
|
|
209
|
+
cf_dir = ensure_cf_directory(project_root)
|
|
210
|
+
console.print(f"[INFO] Generating/updating project.json in {cf_dir}", style="bold cyan")
|
|
211
|
+
project_json_path = update_or_create_project_json(
|
|
212
|
+
cf_dir=str(cf_dir),
|
|
213
|
+
gds_path=collected["gds/user_project_wrapper.gds"],
|
|
214
|
+
cli_overrides=cli_overrides,
|
|
215
|
+
existing_json_path=collected.get(".cf/project.json")
|
|
216
|
+
)
|
|
217
|
+
console.print(f"[OK] project.json ready: {project_json_path}", style="green")
|
|
218
|
+
|
|
219
|
+
# SFTP upload or dry-run
|
|
220
|
+
final_project_name = project_name or (
|
|
221
|
+
cli_overrides.get("project_name") or Path(project_root).name
|
|
222
|
+
)
|
|
223
|
+
sftp_base = f"incoming/projects/{final_project_name}"
|
|
224
|
+
upload_map = {
|
|
225
|
+
".cf/project.json": project_json_path,
|
|
226
|
+
"gds/user_project_wrapper.gds": collected["gds/user_project_wrapper.gds"],
|
|
227
|
+
"verilog/rtl/user_defines.v": collected["verilog/rtl/user_defines.v"],
|
|
228
|
+
}
|
|
229
|
+
if dry_run:
|
|
230
|
+
console.print("[DRY-RUN] The following files would be uploaded:", style="bold magenta")
|
|
231
|
+
for rel_path, local_path in upload_map.items():
|
|
232
|
+
if local_path:
|
|
233
|
+
remote_path = os.path.join(sftp_base, rel_path)
|
|
234
|
+
console.print(f" {local_path} -> {remote_path}", style="magenta")
|
|
235
|
+
console.print("[DRY-RUN] No files were uploaded.", style="bold magenta")
|
|
236
|
+
return
|
|
237
|
+
|
|
238
|
+
console.print(f"[INFO] Connecting to SFTP: {sftp_host} as {sftp_username}", style="bold cyan")
|
|
239
|
+
transport = None
|
|
240
|
+
try:
|
|
241
|
+
sftp, transport = sftp_connect(
|
|
242
|
+
host=sftp_host,
|
|
243
|
+
username=sftp_username,
|
|
244
|
+
password=password,
|
|
245
|
+
key_path=key_path
|
|
246
|
+
)
|
|
247
|
+
# Ensure the project directory exists before uploading
|
|
248
|
+
sftp_project_dir = f"incoming/projects/{final_project_name}"
|
|
249
|
+
sftp_ensure_dirs(sftp, sftp_project_dir)
|
|
250
|
+
except Exception as e:
|
|
251
|
+
console.print(f"[ERROR] Failed to connect to SFTP: {e}", style="bold red")
|
|
252
|
+
raise click.Abort()
|
|
253
|
+
try:
|
|
254
|
+
for rel_path, local_path in upload_map.items():
|
|
255
|
+
if local_path:
|
|
256
|
+
remote_path = os.path.join(sftp_base, rel_path)
|
|
257
|
+
upload_with_progress(
|
|
258
|
+
sftp,
|
|
259
|
+
local_path=local_path,
|
|
260
|
+
remote_path=remote_path,
|
|
261
|
+
force_overwrite=force_overwrite
|
|
262
|
+
)
|
|
263
|
+
console.print(f"[SUCCESS] All files uploaded to {sftp_base}", style="bold green")
|
|
264
|
+
except Exception as e:
|
|
265
|
+
console.print(f"[ERROR] SFTP upload failed: {e}", style="bold red")
|
|
266
|
+
raise click.Abort()
|
|
267
|
+
finally:
|
|
268
|
+
if transport:
|
|
269
|
+
sftp.close()
|
|
270
|
+
transport.close()
|
|
271
|
+
|
|
272
|
+
@main.command('pull')
|
|
273
|
+
@click.option('--project-name', required=False, help='Project name to pull results for (defaults to value in .cf/project.json if present).')
|
|
274
|
+
@click.option('--output-dir', required=False, type=click.Path(file_okay=False), help='(Ignored) Local directory to save results (now always sftp-output/<project_name>).')
|
|
275
|
+
@click.option('--sftp-host', default=DEFAULT_SFTP_HOST, show_default=True, help='SFTP server hostname.')
|
|
276
|
+
@click.option('--sftp-username', required=False, help='SFTP username (defaults to config).')
|
|
277
|
+
@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)
|
|
278
|
+
@click.option('--sftp-password', help='SFTP password. If not provided, will prompt securely.', default=None)
|
|
279
|
+
def pull(project_name, output_dir, sftp_host, sftp_username, sftp_key, sftp_password):
|
|
280
|
+
"""Download results/artifacts from SFTP output dir to local sftp-output/<project_name>."""
|
|
281
|
+
# If .cf/project.json exists in cwd, use its project name as default
|
|
282
|
+
_, cwd_project_name = get_project_json_from_cwd()
|
|
283
|
+
if not project_name and cwd_project_name:
|
|
284
|
+
project_name = cwd_project_name
|
|
285
|
+
if not project_name:
|
|
286
|
+
console.print("[bold red]No project name specified and no .cf/project.json found in current directory. Please provide --project-name.[/bold red]")
|
|
287
|
+
raise click.Abort()
|
|
288
|
+
config = load_user_config()
|
|
289
|
+
if not sftp_username:
|
|
290
|
+
sftp_username = config.get("sftp_username")
|
|
291
|
+
if not sftp_username:
|
|
292
|
+
console.print("[bold red]No SFTP username provided and not found in config. Please run 'chipfoundry config' or provide --sftp-username.[/bold red]")
|
|
293
|
+
raise click.Abort()
|
|
294
|
+
if not sftp_key:
|
|
295
|
+
sftp_key = config.get("sftp_key")
|
|
296
|
+
key_path = sftp_key
|
|
297
|
+
password = sftp_password
|
|
298
|
+
if not key_path and not password:
|
|
299
|
+
if os.path.exists(DEFAULT_SSH_KEY):
|
|
300
|
+
key_path = DEFAULT_SSH_KEY
|
|
301
|
+
console.print(f"[INFO] Using default SSH key: {DEFAULT_SSH_KEY}", style="bold cyan")
|
|
302
|
+
else:
|
|
303
|
+
console.print("[WARN] No SFTP key or password provided, and no default key found at ~/.ssh/id_rsa.", style="bold yellow")
|
|
304
|
+
auth_method = click.prompt("Choose authentication method (key/password)", type=click.Choice(['key', 'password']), show_choices=True)
|
|
305
|
+
if auth_method == 'key':
|
|
306
|
+
key_path = click.prompt("Enter path to SFTP private key", type=click.Path(exists=True, dir_okay=False))
|
|
307
|
+
else:
|
|
308
|
+
password = click.prompt("SFTP Password", hide_input=True)
|
|
309
|
+
elif key_path and password:
|
|
310
|
+
console.print("[ERROR] Options --sftp-password and --sftp-key are mutually exclusive.", style="bold red")
|
|
311
|
+
raise click.UsageError("Options --sftp-password and --sftp-key are mutually exclusive.")
|
|
312
|
+
elif not key_path and password:
|
|
313
|
+
pass # password provided
|
|
314
|
+
elif key_path and not password:
|
|
315
|
+
if not os.path.exists(key_path):
|
|
316
|
+
console.print(f"[ERROR] SFTP key file not found: {key_path}", style="bold red")
|
|
317
|
+
raise click.UsageError(f"SFTP key file not found: {key_path}")
|
|
318
|
+
|
|
319
|
+
console.print(f"[INFO] Connecting to SFTP: {sftp_host} as {sftp_username}", style="bold cyan")
|
|
320
|
+
transport = None
|
|
321
|
+
try:
|
|
322
|
+
sftp, transport = sftp_connect(
|
|
323
|
+
host=sftp_host,
|
|
324
|
+
username=sftp_username,
|
|
325
|
+
password=password,
|
|
326
|
+
key_path=key_path
|
|
327
|
+
)
|
|
328
|
+
except Exception as e:
|
|
329
|
+
console.print(f"[ERROR] Failed to connect to SFTP: {e}", style="bold red")
|
|
330
|
+
raise click.Abort()
|
|
331
|
+
try:
|
|
332
|
+
remote_dir = f"outgoing/results/{project_name}"
|
|
333
|
+
output_dir = os.path.join(os.getcwd(), "sftp-output", project_name)
|
|
334
|
+
os.makedirs(output_dir, exist_ok=True)
|
|
335
|
+
try:
|
|
336
|
+
files = sftp.listdir(remote_dir)
|
|
337
|
+
except Exception:
|
|
338
|
+
console.print(f"[yellow]No results found for project '{project_name}' on SFTP server.[/yellow]")
|
|
339
|
+
return
|
|
340
|
+
if not files:
|
|
341
|
+
console.print(f"[yellow]No files to download for project '{project_name}'.[/yellow]")
|
|
342
|
+
return
|
|
343
|
+
with Progress(
|
|
344
|
+
TextColumn("[progress.description]{task.description}"),
|
|
345
|
+
BarColumn(),
|
|
346
|
+
TaskProgressColumn(),
|
|
347
|
+
TextColumn("{task.percentage:>3.0f}%"),
|
|
348
|
+
TextColumn("•"),
|
|
349
|
+
TextColumn("{task.completed}/{task.total} bytes"),
|
|
350
|
+
TimeElapsedColumn(),
|
|
351
|
+
) as progress:
|
|
352
|
+
for fname in files:
|
|
353
|
+
remote_path = f"{remote_dir}/{fname}"
|
|
354
|
+
local_path = os.path.join(output_dir, fname)
|
|
355
|
+
try:
|
|
356
|
+
file_size = sftp.stat(remote_path).st_size
|
|
357
|
+
task = progress.add_task(f"Downloading {fname}", total=file_size)
|
|
358
|
+
with open(local_path, "wb") as f:
|
|
359
|
+
def callback(bytes_transferred, total=file_size):
|
|
360
|
+
progress.update(task, completed=bytes_transferred)
|
|
361
|
+
sftp.getfo(remote_path, f, callback=callback)
|
|
362
|
+
progress.update(task, completed=file_size)
|
|
363
|
+
except Exception as e:
|
|
364
|
+
console.print(f"[red]Failed to download {fname}: {e}[/red]")
|
|
365
|
+
console.print(f"[green]All files downloaded to {output_dir}[/green]")
|
|
366
|
+
finally:
|
|
367
|
+
if transport:
|
|
368
|
+
sftp.close()
|
|
369
|
+
transport.close()
|
|
370
|
+
|
|
371
|
+
@main.command('status')
|
|
372
|
+
@click.option('--sftp-host', default=DEFAULT_SFTP_HOST, show_default=True, help='SFTP server hostname.')
|
|
373
|
+
@click.option('--sftp-username', required=False, help='SFTP username (defaults to config).')
|
|
374
|
+
@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)
|
|
375
|
+
@click.option('--sftp-password', help='SFTP password. If not provided, will prompt securely.', default=None)
|
|
376
|
+
def status(sftp_host, sftp_username, sftp_key, sftp_password):
|
|
377
|
+
"""Show all projects and outputs for the user on the SFTP server."""
|
|
378
|
+
config = load_user_config()
|
|
379
|
+
if not sftp_username:
|
|
380
|
+
sftp_username = config.get("sftp_username")
|
|
381
|
+
if not sftp_username:
|
|
382
|
+
console.print("[bold red]No SFTP username provided and not found in config. Please run 'chipfoundry init' or provide --sftp-username.[/bold red]")
|
|
383
|
+
raise click.Abort()
|
|
384
|
+
if not sftp_key:
|
|
385
|
+
sftp_key = config.get("sftp_key")
|
|
386
|
+
key_path = sftp_key
|
|
387
|
+
password = sftp_password
|
|
388
|
+
if not key_path and not password:
|
|
389
|
+
if os.path.exists(DEFAULT_SSH_KEY):
|
|
390
|
+
key_path = DEFAULT_SSH_KEY
|
|
391
|
+
console.print(f"[INFO] Using default SSH key: {DEFAULT_SSH_KEY}", style="bold cyan")
|
|
392
|
+
else:
|
|
393
|
+
console.print("[WARN] No SFTP key or password provided, and no default key found at ~/.ssh/id_rsa.", style="bold yellow")
|
|
394
|
+
auth_method = click.prompt("Choose authentication method (key/password)", type=click.Choice(['key', 'password']), show_choices=True)
|
|
395
|
+
if auth_method == 'key':
|
|
396
|
+
key_path = click.prompt("Enter path to SFTP private key", type=click.Path(exists=True, dir_okay=False))
|
|
397
|
+
else:
|
|
398
|
+
password = click.prompt("SFTP Password", hide_input=True)
|
|
399
|
+
elif key_path and password:
|
|
400
|
+
console.print("[ERROR] Options --sftp-password and --sftp-key are mutually exclusive.", style="bold red")
|
|
401
|
+
raise click.UsageError("Options --sftp-password and --sftp-key are mutually exclusive.")
|
|
402
|
+
elif not key_path and password:
|
|
403
|
+
pass # password provided
|
|
404
|
+
elif key_path and not password:
|
|
405
|
+
if not os.path.exists(key_path):
|
|
406
|
+
console.print(f"[ERROR] SFTP key file not found: {key_path}", style="bold red")
|
|
407
|
+
raise click.UsageError(f"SFTP key file not found: {key_path}")
|
|
408
|
+
|
|
409
|
+
console.print(f"[INFO] Connecting to SFTP: {sftp_host} as {sftp_username}", style="bold cyan")
|
|
410
|
+
transport = None
|
|
411
|
+
try:
|
|
412
|
+
sftp, transport = sftp_connect(
|
|
413
|
+
host=sftp_host,
|
|
414
|
+
username=sftp_username,
|
|
415
|
+
password=password,
|
|
416
|
+
key_path=key_path
|
|
417
|
+
)
|
|
418
|
+
except Exception as e:
|
|
419
|
+
console.print(f"[ERROR] Failed to connect to SFTP: {e}", style="bold red")
|
|
420
|
+
raise click.Abort()
|
|
421
|
+
try:
|
|
422
|
+
# List projects in incoming/projects/ and outgoing/results/
|
|
423
|
+
incoming_projects_dir = f"incoming/projects"
|
|
424
|
+
outgoing_results_dir = f"outgoing/results"
|
|
425
|
+
projects = []
|
|
426
|
+
results = []
|
|
427
|
+
try:
|
|
428
|
+
projects = sftp.listdir(incoming_projects_dir)
|
|
429
|
+
except Exception:
|
|
430
|
+
pass
|
|
431
|
+
try:
|
|
432
|
+
results = sftp.listdir(outgoing_results_dir)
|
|
433
|
+
except Exception:
|
|
434
|
+
pass
|
|
435
|
+
table = Table(title=f"SFTP Status for {sftp_username}")
|
|
436
|
+
table.add_column("Project Name", style="cyan", no_wrap=True)
|
|
437
|
+
table.add_column("Has Input", style="yellow")
|
|
438
|
+
table.add_column("Has Output", style="green")
|
|
439
|
+
all_projects = set(projects) | set(results)
|
|
440
|
+
for proj in sorted(all_projects):
|
|
441
|
+
has_input = "Yes" if proj in projects else "No"
|
|
442
|
+
has_output = "Yes" if proj in results else "No"
|
|
443
|
+
table.add_row(proj, has_input, has_output)
|
|
444
|
+
if all_projects:
|
|
445
|
+
console.print(table)
|
|
446
|
+
else:
|
|
447
|
+
console.print("[yellow]No projects or results found on SFTP server.[/yellow]")
|
|
448
|
+
finally:
|
|
449
|
+
if transport:
|
|
450
|
+
sftp.close()
|
|
451
|
+
transport.close()
|
|
452
|
+
|
|
453
|
+
if __name__ == "__main__":
|
|
454
|
+
main()
|
|
@@ -6,6 +6,7 @@ import json
|
|
|
6
6
|
import hashlib
|
|
7
7
|
import paramiko
|
|
8
8
|
from rich.progress import Progress, BarColumn, TextColumn, TimeElapsedColumn, TaskProgressColumn
|
|
9
|
+
import toml
|
|
9
10
|
|
|
10
11
|
REQUIRED_FILES = {
|
|
11
12
|
".cf/project.json": False, # Optional, may not exist
|
|
@@ -177,4 +178,19 @@ def upload_with_progress(sftp, local_path, remote_path, force_overwrite=False):
|
|
|
177
178
|
progress.update(task, completed=bytes_transferred)
|
|
178
179
|
result = sftp_upload_file(sftp, local_path, remote_path, force_overwrite, progress_cb=progress_cb)
|
|
179
180
|
progress.update(task, completed=file_size)
|
|
180
|
-
return result
|
|
181
|
+
return result
|
|
182
|
+
|
|
183
|
+
def get_config_path() -> Path:
|
|
184
|
+
return Path.home() / ".chipfoundry-cli" / "config.toml"
|
|
185
|
+
|
|
186
|
+
def load_user_config() -> dict:
|
|
187
|
+
config_path = get_config_path()
|
|
188
|
+
if config_path.exists():
|
|
189
|
+
return toml.load(config_path)
|
|
190
|
+
return {}
|
|
191
|
+
|
|
192
|
+
def save_user_config(config: dict):
|
|
193
|
+
config_path = get_config_path()
|
|
194
|
+
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
195
|
+
with open(config_path, 'w') as f:
|
|
196
|
+
toml.dump(config, f)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "chipfoundry-cli"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.2"
|
|
4
4
|
description = "CLI tool to automate ChipFoundry project submission to SFTP server"
|
|
5
5
|
authors = ["ChipFoundry <marwan.abbas@chipfoundry.io>"]
|
|
6
6
|
readme = "README.md"
|
|
@@ -14,6 +14,7 @@ python = ">=3.8.0"
|
|
|
14
14
|
click = ">=8.0.0,<9"
|
|
15
15
|
rich = ">=13,<14"
|
|
16
16
|
paramiko = ">=3.0.0,<4"
|
|
17
|
+
toml = ">=0.10,<1.0"
|
|
17
18
|
|
|
18
19
|
[tool.poetry.dev-dependencies]
|
|
19
20
|
wheel = "*"
|
|
@@ -22,6 +23,7 @@ flake8 = ">=4"
|
|
|
22
23
|
|
|
23
24
|
[tool.poetry.scripts]
|
|
24
25
|
chipfoundry = "chipfoundry_cli.main:main"
|
|
26
|
+
cf = "chipfoundry_cli.main:main"
|
|
25
27
|
|
|
26
28
|
[build-system]
|
|
27
29
|
requires = ["poetry-core>=1.0.0"]
|
|
@@ -1,173 +0,0 @@
|
|
|
1
|
-
import click
|
|
2
|
-
import getpass
|
|
3
|
-
from chipfoundry_cli.utils import (
|
|
4
|
-
collect_project_files, ensure_cf_directory, update_or_create_project_json,
|
|
5
|
-
sftp_connect, upload_with_progress, sftp_ensure_dirs
|
|
6
|
-
)
|
|
7
|
-
import os
|
|
8
|
-
from pathlib import Path
|
|
9
|
-
from rich.console import Console
|
|
10
|
-
from rich.panel import Panel
|
|
11
|
-
from rich.text import Text
|
|
12
|
-
|
|
13
|
-
DEFAULT_SSH_KEY = os.path.expanduser('~/.ssh/id_rsa')
|
|
14
|
-
DEFAULT_SFTP_HOST = 'sftp.chipfoundry.io'
|
|
15
|
-
|
|
16
|
-
GDS_TYPE_MAP = {
|
|
17
|
-
'user_project_wrapper.gds': 'digital',
|
|
18
|
-
'user_analog_project_wrapper.gds': 'analog',
|
|
19
|
-
'openframe_project_wrapper.gds': 'openframe',
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
console = Console()
|
|
23
|
-
|
|
24
|
-
@click.group(help="ChipFoundry CLI: Automate project submission and management.")
|
|
25
|
-
def main():
|
|
26
|
-
pass
|
|
27
|
-
|
|
28
|
-
@main.command('submit')
|
|
29
|
-
@click.option('--project-root', required=True, type=click.Path(exists=True, file_okay=False), help='Path to the local ChipFoundry project directory.')
|
|
30
|
-
@click.option('--sftp-host', default=DEFAULT_SFTP_HOST, show_default=True, help='SFTP server hostname.')
|
|
31
|
-
@click.option('--sftp-username', required=True, help='SFTP username.')
|
|
32
|
-
@click.option('--sftp-key', type=click.Path(exists=True, dir_okay=False), help='Path to SFTP private key file. Defaults to ~/.ssh/id_rsa if it exists.', default=None, show_default=False)
|
|
33
|
-
@click.option('--sftp-password', help='SFTP password. If not provided, will prompt securely.', default=None)
|
|
34
|
-
@click.option('--project-id', help='Project ID (e.g., "user123_proj456"). Overrides project.json if exists.')
|
|
35
|
-
@click.option('--project-name', help='Project name (e.g., "my_project"). Overrides project.json if exists.')
|
|
36
|
-
@click.option('--project-type', help='Project type (auto-detected if not provided).', default=None)
|
|
37
|
-
@click.option('--force-overwrite', is_flag=True, help='Overwrite existing files on SFTP without prompting.')
|
|
38
|
-
@click.option('--dry-run', is_flag=True, help='Preview actions without uploading files.')
|
|
39
|
-
def submit(project_root, sftp_host, sftp_username, sftp_key, sftp_password, project_id, project_name, project_type, force_overwrite, dry_run):
|
|
40
|
-
"""Submit a project to the SFTP server."""
|
|
41
|
-
# Determine which authentication method to use
|
|
42
|
-
key_path = sftp_key
|
|
43
|
-
password = sftp_password
|
|
44
|
-
# If neither provided, try default key
|
|
45
|
-
if not key_path and not password:
|
|
46
|
-
if os.path.exists(DEFAULT_SSH_KEY):
|
|
47
|
-
key_path = DEFAULT_SSH_KEY
|
|
48
|
-
console.print(f"[INFO] Using default SSH key: {DEFAULT_SSH_KEY}", style="bold cyan")
|
|
49
|
-
else:
|
|
50
|
-
console.print("[WARN] No SFTP key or password provided, and no default key found at ~/.ssh/id_rsa.", style="bold yellow")
|
|
51
|
-
auth_method = click.prompt("Choose authentication method (key/password)", type=click.Choice(['key', 'password']), show_choices=True)
|
|
52
|
-
if auth_method == 'key':
|
|
53
|
-
key_path = click.prompt("Enter path to SFTP private key", type=click.Path(exists=True, dir_okay=False))
|
|
54
|
-
else:
|
|
55
|
-
password = click.prompt("SFTP Password", hide_input=True)
|
|
56
|
-
elif key_path and password:
|
|
57
|
-
console.print("[ERROR] Options --sftp-password and --sftp-key are mutually exclusive.", style="bold red")
|
|
58
|
-
raise click.UsageError("Options --sftp-password and --sftp-key are mutually exclusive.")
|
|
59
|
-
elif not key_path and password:
|
|
60
|
-
pass # password provided
|
|
61
|
-
elif key_path and not password:
|
|
62
|
-
if not os.path.exists(key_path):
|
|
63
|
-
console.print(f"[ERROR] SFTP key file not found: {key_path}", style="bold red")
|
|
64
|
-
raise click.UsageError(f"SFTP key file not found: {key_path}")
|
|
65
|
-
|
|
66
|
-
console.print(f"[INFO] Collecting project files from: {project_root}", style="bold cyan")
|
|
67
|
-
try:
|
|
68
|
-
collected = collect_project_files(project_root)
|
|
69
|
-
for rel_path, abs_path in collected.items():
|
|
70
|
-
if abs_path:
|
|
71
|
-
console.print(f"[OK] Found: {rel_path} -> {abs_path}", style="green")
|
|
72
|
-
else:
|
|
73
|
-
console.print(f"[INFO] Optional file not found: {rel_path}", style="yellow")
|
|
74
|
-
except FileNotFoundError as e:
|
|
75
|
-
console.print(f"[ERROR] {e}", style="bold red")
|
|
76
|
-
raise click.Abort()
|
|
77
|
-
|
|
78
|
-
# Auto-detect project type from GDS file name if not provided
|
|
79
|
-
gds_dir = Path(project_root) / 'gds'
|
|
80
|
-
found_types = []
|
|
81
|
-
gds_file_path = None
|
|
82
|
-
for gds_name, gds_type in GDS_TYPE_MAP.items():
|
|
83
|
-
candidate = gds_dir / gds_name
|
|
84
|
-
if candidate.exists():
|
|
85
|
-
found_types.append(gds_type)
|
|
86
|
-
gds_file_path = str(candidate)
|
|
87
|
-
if project_type:
|
|
88
|
-
detected_type = project_type
|
|
89
|
-
else:
|
|
90
|
-
if len(found_types) == 0:
|
|
91
|
-
console.print("[ERROR] No recognized GDS file found for project type detection.", style="bold red")
|
|
92
|
-
raise click.Abort()
|
|
93
|
-
elif len(found_types) > 1:
|
|
94
|
-
console.print(f"[ERROR] Multiple GDS types found: {found_types}. Only one project type is allowed per project.", style="bold red")
|
|
95
|
-
raise click.Abort()
|
|
96
|
-
else:
|
|
97
|
-
detected_type = found_types[0]
|
|
98
|
-
console.print(f"[INFO] Detected project type: {detected_type}", style="bold cyan")
|
|
99
|
-
# Use the detected GDS file for upload and hash
|
|
100
|
-
if gds_file_path:
|
|
101
|
-
collected['gds/user_project_wrapper.gds'] = gds_file_path
|
|
102
|
-
# Prepare CLI overrides for project.json
|
|
103
|
-
cli_overrides = {
|
|
104
|
-
"project_id": project_id,
|
|
105
|
-
"project_name": project_name,
|
|
106
|
-
"project_type": detected_type,
|
|
107
|
-
"sftp_username": sftp_username,
|
|
108
|
-
}
|
|
109
|
-
cf_dir = ensure_cf_directory(project_root)
|
|
110
|
-
console.print(f"[INFO] Generating/updating project.json in {cf_dir}", style="bold cyan")
|
|
111
|
-
project_json_path = update_or_create_project_json(
|
|
112
|
-
cf_dir=str(cf_dir),
|
|
113
|
-
gds_path=collected["gds/user_project_wrapper.gds"],
|
|
114
|
-
cli_overrides=cli_overrides,
|
|
115
|
-
existing_json_path=collected.get(".cf/project.json")
|
|
116
|
-
)
|
|
117
|
-
console.print(f"[OK] project.json ready: {project_json_path}", style="green")
|
|
118
|
-
|
|
119
|
-
# SFTP upload or dry-run
|
|
120
|
-
final_project_name = project_name or (
|
|
121
|
-
cli_overrides.get("project_name") or Path(project_root).name
|
|
122
|
-
)
|
|
123
|
-
sftp_base = f"incoming/projects/{final_project_name}"
|
|
124
|
-
upload_map = {
|
|
125
|
-
".cf/project.json": project_json_path,
|
|
126
|
-
"gds/user_project_wrapper.gds": collected["gds/user_project_wrapper.gds"],
|
|
127
|
-
"verilog/rtl/user_defines.v": collected["verilog/rtl/user_defines.v"],
|
|
128
|
-
}
|
|
129
|
-
if dry_run:
|
|
130
|
-
console.print("[DRY-RUN] The following files would be uploaded:", style="bold magenta")
|
|
131
|
-
for rel_path, local_path in upload_map.items():
|
|
132
|
-
if local_path:
|
|
133
|
-
remote_path = os.path.join(sftp_base, rel_path)
|
|
134
|
-
console.print(f" {local_path} -> {remote_path}", style="magenta")
|
|
135
|
-
console.print("[DRY-RUN] No files were uploaded.", style="bold magenta")
|
|
136
|
-
return
|
|
137
|
-
|
|
138
|
-
console.print(f"[INFO] Connecting to SFTP: {sftp_host} as {sftp_username}", style="bold cyan")
|
|
139
|
-
transport = None
|
|
140
|
-
try:
|
|
141
|
-
sftp, transport = sftp_connect(
|
|
142
|
-
host=sftp_host,
|
|
143
|
-
username=sftp_username,
|
|
144
|
-
password=password,
|
|
145
|
-
key_path=key_path
|
|
146
|
-
)
|
|
147
|
-
# Ensure the project directory exists before uploading
|
|
148
|
-
sftp_project_dir = f"incoming/projects/{final_project_name}"
|
|
149
|
-
sftp_ensure_dirs(sftp, sftp_project_dir)
|
|
150
|
-
except Exception as e:
|
|
151
|
-
console.print(f"[ERROR] Failed to connect to SFTP: {e}", style="bold red")
|
|
152
|
-
raise click.Abort()
|
|
153
|
-
try:
|
|
154
|
-
for rel_path, local_path in upload_map.items():
|
|
155
|
-
if local_path:
|
|
156
|
-
remote_path = os.path.join(sftp_base, rel_path)
|
|
157
|
-
upload_with_progress(
|
|
158
|
-
sftp,
|
|
159
|
-
local_path=local_path,
|
|
160
|
-
remote_path=remote_path,
|
|
161
|
-
force_overwrite=force_overwrite
|
|
162
|
-
)
|
|
163
|
-
console.print(f"[SUCCESS] All files uploaded to {sftp_base}", style="bold green")
|
|
164
|
-
except Exception as e:
|
|
165
|
-
console.print(f"[ERROR] SFTP upload failed: {e}", style="bold red")
|
|
166
|
-
raise click.Abort()
|
|
167
|
-
finally:
|
|
168
|
-
if transport:
|
|
169
|
-
sftp.close()
|
|
170
|
-
transport.close()
|
|
171
|
-
|
|
172
|
-
if __name__ == "__main__":
|
|
173
|
-
main()
|
|
File without changes
|
|
File without changes
|