fabricgate 0.1.0__py3-none-any.whl
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.
- fabricgate/_version.py +4 -0
- fabricgate/cli/__init__.py +1 -0
- fabricgate/cli/commands/__init__.py +1 -0
- fabricgate/cli/commands/build.py +359 -0
- fabricgate/cli/commands/deprecate.py +77 -0
- fabricgate/cli/commands/diff.py +123 -0
- fabricgate/cli/commands/info.py +42 -0
- fabricgate/cli/commands/license_check.py +76 -0
- fabricgate/cli/commands/list_cmd.py +44 -0
- fabricgate/cli/commands/login.py +41 -0
- fabricgate/cli/commands/pull.py +80 -0
- fabricgate/cli/commands/push.py +162 -0
- fabricgate/cli/commands/quota.py +66 -0
- fabricgate/cli/commands/search.py +58 -0
- fabricgate/cli/commands/stats.py +95 -0
- fabricgate/cli/commands/token.py +106 -0
- fabricgate/cli/commands/verify.py +38 -0
- fabricgate/cli/commands/watch.py +58 -0
- fabricgate/cli/commands/webhook.py +166 -0
- fabricgate/cli/commands/yank.py +49 -0
- fabricgate/cli/exit_codes.py +28 -0
- fabricgate/cli/main.py +83 -0
- fabricgate/client/__init__.py +44 -0
- fabricgate/client/auth.py +108 -0
- fabricgate/client/cache.py +97 -0
- fabricgate/client/config.py +52 -0
- fabricgate/client/errors.py +27 -0
- fabricgate/client/publisher.py +147 -0
- fabricgate/client/puller.py +382 -0
- fabricgate/client/registry_client.py +649 -0
- fabricgate/client/resolver.py +93 -0
- fabricgate/client/validator.py +30 -0
- fabricgate/data/__init__.py +0 -0
- fabricgate/data/board-db/.gitkeep +0 -0
- fabricgate/data/board-db/official.yaml +79 -0
- fabricgate/models/__init__.py +10 -0
- fabricgate/models/api/__init__.py +1 -0
- fabricgate/models/api/errors.py +74 -0
- fabricgate/models/api/members.py +49 -0
- fabricgate/models/api/requests.py +211 -0
- fabricgate/models/api/responses.py +672 -0
- fabricgate/models/board_db.py +210 -0
- fabricgate/models/cli.py +312 -0
- fabricgate/models/common.py +390 -0
- fabricgate/models/design_index.py +110 -0
- fabricgate/models/platform_manifest.py +394 -0
- fabricgate/py.typed +1 -0
- fabricgate/sdk/__init__.py +5 -0
- fabricgate/sdk/_helpers.py +218 -0
- fabricgate/sdk/api.py +72 -0
- fabricgate/sdk/auth.py +231 -0
- fabricgate/sdk/designs/__init__.py +35 -0
- fabricgate/sdk/designs/diff.py +25 -0
- fabricgate/sdk/designs/license.py +69 -0
- fabricgate/sdk/designs/lifecycle.py +198 -0
- fabricgate/sdk/designs/pull.py +142 -0
- fabricgate/sdk/designs/push.py +163 -0
- fabricgate/sdk/designs/quota.py +41 -0
- fabricgate/sdk/designs/search.py +69 -0
- fabricgate/sdk/designs/stats.py +105 -0
- fabricgate/sdk/designs/watch.py +101 -0
- fabricgate/sdk/hwh_parser.py +152 -0
- fabricgate/sdk/webhooks.py +211 -0
- fabricgate-0.1.0.dist-info/METADATA +122 -0
- fabricgate-0.1.0.dist-info/RECORD +68 -0
- fabricgate-0.1.0.dist-info/WHEEL +4 -0
- fabricgate-0.1.0.dist-info/entry_points.txt +3 -0
- fabricgate-0.1.0.dist-info/licenses/LICENSE +21 -0
fabricgate/_version.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""FabricGate CLI (Click)."""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""CLI command implementations."""
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
"""fabricgate build command — generate Platform Manifest and Design Index from a HWH file."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import click
|
|
9
|
+
import yaml
|
|
10
|
+
|
|
11
|
+
from fabricgate.models.board_db import BoardDb
|
|
12
|
+
from fabricgate.models.platform_manifest import Interface
|
|
13
|
+
from fabricgate.sdk.hwh_parser import HwhInfo, HwhParseError, ParsedInterface, find_hwh_files, parse_hwh
|
|
14
|
+
|
|
15
|
+
# ---------------------------------------------------------------------------
|
|
16
|
+
# Board resolution helpers
|
|
17
|
+
# ---------------------------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _stdin_is_tty() -> bool:
|
|
21
|
+
"""Whether interactive prompts are possible (patched in tests)."""
|
|
22
|
+
return sys.stdin.isatty()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _resolve_board_id(
|
|
26
|
+
hwh_info: HwhInfo,
|
|
27
|
+
db: BoardDb,
|
|
28
|
+
) -> tuple[str, str | None]:
|
|
29
|
+
"""Return ``(board_id, device_family)`` from *hwh_info* and *db*.
|
|
30
|
+
|
|
31
|
+
Resolution order:
|
|
32
|
+
1. BOARDPART lookup in Board DB.
|
|
33
|
+
2. Virtual board based on *design_portability*.
|
|
34
|
+
"""
|
|
35
|
+
if hwh_info.boardpart:
|
|
36
|
+
entry = db.lookup_by_boardpart(hwh_info.boardpart)
|
|
37
|
+
if entry:
|
|
38
|
+
return entry.board_id, entry.device_family
|
|
39
|
+
|
|
40
|
+
portability = hwh_info.design_portability
|
|
41
|
+
df = hwh_info.device_family
|
|
42
|
+
pkg = hwh_info.package
|
|
43
|
+
|
|
44
|
+
if portability == "board-specific" and df and pkg:
|
|
45
|
+
return f"custom-{df}-{pkg}", df
|
|
46
|
+
if portability == "board-specific" and df:
|
|
47
|
+
return f"custom-{df}", df
|
|
48
|
+
# axi-only or io-constrained
|
|
49
|
+
if df:
|
|
50
|
+
return f"generic-{df}", df
|
|
51
|
+
return "generic-unknown", None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _interfaces_to_manifest(parsed: list[ParsedInterface]) -> list[Interface]:
|
|
55
|
+
"""Convert parsed HWH interfaces to manifest :class:`Interface` objects."""
|
|
56
|
+
result: list[Interface] = []
|
|
57
|
+
for p in parsed:
|
|
58
|
+
iface_type = "axi-lite"
|
|
59
|
+
if p.protocol:
|
|
60
|
+
proto = p.protocol.lower()
|
|
61
|
+
if "stream" in proto:
|
|
62
|
+
iface_type = "axi-stream"
|
|
63
|
+
elif "lite" in proto:
|
|
64
|
+
iface_type = "axi-lite"
|
|
65
|
+
elif "axi4" in proto:
|
|
66
|
+
iface_type = "axi"
|
|
67
|
+
elif "stream" in p.vlnv.lower() or "axis_rtl" in p.vlnv.lower():
|
|
68
|
+
iface_type = "axi-stream"
|
|
69
|
+
result.append(
|
|
70
|
+
Interface(
|
|
71
|
+
name=p.name,
|
|
72
|
+
type=iface_type,
|
|
73
|
+
base=p.base_address,
|
|
74
|
+
)
|
|
75
|
+
)
|
|
76
|
+
return result
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# ---------------------------------------------------------------------------
|
|
80
|
+
# YAML generation
|
|
81
|
+
# ---------------------------------------------------------------------------
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _make_manifest_stub(
|
|
85
|
+
*,
|
|
86
|
+
runtime: str,
|
|
87
|
+
board: str,
|
|
88
|
+
device_family: str | None,
|
|
89
|
+
design_ref: str,
|
|
90
|
+
interfaces: list[Interface],
|
|
91
|
+
design_portability: str,
|
|
92
|
+
) -> str:
|
|
93
|
+
"""Return a YAML string for a platform manifest stub.
|
|
94
|
+
|
|
95
|
+
Artifact hashes are placeholder values; the user must replace them
|
|
96
|
+
before running ``fabricgate push``.
|
|
97
|
+
"""
|
|
98
|
+
placeholder = "a" * 64
|
|
99
|
+
data: dict[str, object] = {
|
|
100
|
+
"schema": "fabricgate-platform/v1",
|
|
101
|
+
"runtime": runtime,
|
|
102
|
+
"board": board,
|
|
103
|
+
"design_ref": design_ref,
|
|
104
|
+
"artifacts": {
|
|
105
|
+
"bitstream": {"file": "design.bit", "sha256": placeholder},
|
|
106
|
+
"hwh": {"file": "design.hwh", "sha256": placeholder},
|
|
107
|
+
},
|
|
108
|
+
}
|
|
109
|
+
if device_family:
|
|
110
|
+
data["device_family"] = device_family
|
|
111
|
+
data["design_portability"] = design_portability
|
|
112
|
+
if interfaces:
|
|
113
|
+
data["interfaces"] = [
|
|
114
|
+
{k: v for k, v in {"name": i.name, "type": i.type, "base": i.base}.items() if v is not None}
|
|
115
|
+
for i in interfaces
|
|
116
|
+
]
|
|
117
|
+
return yaml.safe_dump(data, sort_keys=False, allow_unicode=True)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _make_index_stub(
|
|
121
|
+
*,
|
|
122
|
+
design_name: str,
|
|
123
|
+
version: str,
|
|
124
|
+
summary: str | None,
|
|
125
|
+
tags: list[str],
|
|
126
|
+
platform_id: str,
|
|
127
|
+
) -> str:
|
|
128
|
+
"""Return a YAML string for a design index stub."""
|
|
129
|
+
placeholder = "sha256:" + "a" * 64
|
|
130
|
+
data: dict[str, object] = {
|
|
131
|
+
"schema": "fabricgate-index/v1",
|
|
132
|
+
"name": design_name,
|
|
133
|
+
"version": version,
|
|
134
|
+
"platforms": [{"platform": platform_id, "digest": placeholder}],
|
|
135
|
+
}
|
|
136
|
+
if summary:
|
|
137
|
+
data["summary"] = summary
|
|
138
|
+
if tags:
|
|
139
|
+
data["tags"] = tags
|
|
140
|
+
return yaml.safe_dump(data, sort_keys=False, allow_unicode=True)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
# ---------------------------------------------------------------------------
|
|
144
|
+
# CLI command
|
|
145
|
+
# ---------------------------------------------------------------------------
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
@click.command(name="build")
|
|
149
|
+
@click.argument("directory", type=click.Path(exists=True, file_okay=False, path_type=Path))
|
|
150
|
+
@click.option("--namespace", default=None, help="Registry namespace (interactive if omitted).")
|
|
151
|
+
@click.option("--version", "semver", default=None, help="SemVer version (interactive if omitted).")
|
|
152
|
+
@click.option("--tags", default=None, help="Comma-separated tags (e.g. dma,axi,zynq).")
|
|
153
|
+
@click.option("--dry-run", is_flag=True, help="Preview output without writing files.")
|
|
154
|
+
@click.option(
|
|
155
|
+
"--output",
|
|
156
|
+
"output_dir",
|
|
157
|
+
default=None,
|
|
158
|
+
type=click.Path(file_okay=False, path_type=Path),
|
|
159
|
+
help="Output directory (default: <design_slug>/ in current directory).",
|
|
160
|
+
)
|
|
161
|
+
@click.option(
|
|
162
|
+
"--hwh",
|
|
163
|
+
"hwh_path_opt",
|
|
164
|
+
default=None,
|
|
165
|
+
type=click.Path(exists=True, dir_okay=False, path_type=Path),
|
|
166
|
+
help="Explicit path to .hwh file (required when multiple .hwh files are found).",
|
|
167
|
+
)
|
|
168
|
+
def build_cmd(
|
|
169
|
+
directory: Path,
|
|
170
|
+
namespace: str | None,
|
|
171
|
+
semver: str | None,
|
|
172
|
+
tags: str | None,
|
|
173
|
+
dry_run: bool,
|
|
174
|
+
output_dir: Path | None,
|
|
175
|
+
hwh_path_opt: Path | None,
|
|
176
|
+
) -> None:
|
|
177
|
+
"""Generate Platform Manifest and Design Index from a Vivado HWH file."""
|
|
178
|
+
# --- Step 1: Find HWH files ---
|
|
179
|
+
click.echo("Scanning for HWH files ...")
|
|
180
|
+
hwh_files = find_hwh_files(directory)
|
|
181
|
+
if not hwh_files:
|
|
182
|
+
click.echo(f"Error: No .hwh files found in {directory}", err=True)
|
|
183
|
+
raise click.exceptions.Exit(1)
|
|
184
|
+
for f in hwh_files:
|
|
185
|
+
click.echo(f" Found: {f.relative_to(directory.parent)}")
|
|
186
|
+
|
|
187
|
+
if hwh_path_opt is not None:
|
|
188
|
+
hwh_path = hwh_path_opt
|
|
189
|
+
elif len(hwh_files) > 1:
|
|
190
|
+
if not _stdin_is_tty():
|
|
191
|
+
# Non-interactive: error — user must specify via --hwh
|
|
192
|
+
names = ", ".join(f.name for f in hwh_files)
|
|
193
|
+
click.echo(
|
|
194
|
+
f"\nError: Multiple HWH files found ({names}). Use --hwh to specify one.",
|
|
195
|
+
err=True,
|
|
196
|
+
)
|
|
197
|
+
raise click.exceptions.Exit(1)
|
|
198
|
+
else:
|
|
199
|
+
click.echo("\nMultiple HWH files found. Select one:")
|
|
200
|
+
for i, f in enumerate(hwh_files):
|
|
201
|
+
click.echo(f" [{i + 1}] {f.relative_to(directory.parent)}")
|
|
202
|
+
choice = click.prompt(
|
|
203
|
+
"Select HWH file",
|
|
204
|
+
type=click.IntRange(1, len(hwh_files)),
|
|
205
|
+
default=1,
|
|
206
|
+
)
|
|
207
|
+
hwh_path = hwh_files[choice - 1]
|
|
208
|
+
else:
|
|
209
|
+
hwh_path = hwh_files[0]
|
|
210
|
+
|
|
211
|
+
# --- Step 2: Parse HWH ---
|
|
212
|
+
click.echo("\nParsing HWH ...")
|
|
213
|
+
try:
|
|
214
|
+
hwh_info = parse_hwh(hwh_path)
|
|
215
|
+
except HwhParseError as exc:
|
|
216
|
+
click.echo(f"Error: {exc}", err=True)
|
|
217
|
+
raise click.exceptions.Exit(1) from exc
|
|
218
|
+
|
|
219
|
+
if hwh_info.boardpart:
|
|
220
|
+
click.echo(f" BOARDPART: {hwh_info.boardpart}")
|
|
221
|
+
else:
|
|
222
|
+
click.echo(" BOARDPART: (not found)")
|
|
223
|
+
if hwh_info.interfaces:
|
|
224
|
+
axi_count = sum(1 for i in hwh_info.interfaces if i.is_axi)
|
|
225
|
+
bs_count = sum(1 for i in hwh_info.interfaces if i.is_board_specific)
|
|
226
|
+
iface_summary = f"{len(hwh_info.interfaces)} interface(s)"
|
|
227
|
+
if axi_count:
|
|
228
|
+
iface_summary += f", {axi_count} AXI"
|
|
229
|
+
if bs_count:
|
|
230
|
+
bs_names = [i.vlnv.split(":")[-2] for i in hwh_info.interfaces if i.is_board_specific]
|
|
231
|
+
iface_summary += f", board-specific: {', '.join(bs_names)}"
|
|
232
|
+
click.echo(f" EXTERNALINTERFACES: {iface_summary}")
|
|
233
|
+
else:
|
|
234
|
+
click.echo(" EXTERNALINTERFACES: (none)")
|
|
235
|
+
|
|
236
|
+
portability = hwh_info.design_portability
|
|
237
|
+
click.echo(f" → design_portability: {portability}")
|
|
238
|
+
|
|
239
|
+
# --- Step 3: Board DB lookup ---
|
|
240
|
+
try:
|
|
241
|
+
db = BoardDb.load()
|
|
242
|
+
except Exception as exc:
|
|
243
|
+
click.echo(f"Error: Board DB load failed: {exc}", err=True)
|
|
244
|
+
# INFRA: failed to load local Board DB (ADR-008 exit-code standard).
|
|
245
|
+
raise click.exceptions.Exit(4) from exc
|
|
246
|
+
|
|
247
|
+
board_id: str
|
|
248
|
+
device_family: str | None
|
|
249
|
+
|
|
250
|
+
if hwh_info.boardpart:
|
|
251
|
+
entry = db.lookup_by_boardpart(hwh_info.boardpart)
|
|
252
|
+
if entry:
|
|
253
|
+
click.echo(f" → Board DB match: {entry.board_id} ({entry.display_name})")
|
|
254
|
+
board_id = entry.board_id
|
|
255
|
+
device_family = entry.device_family
|
|
256
|
+
else:
|
|
257
|
+
# Not in Board DB — propose virtual board
|
|
258
|
+
click.echo(" → Board DB: not found")
|
|
259
|
+
proposed_board_id, device_family = _resolve_board_id(hwh_info, db)
|
|
260
|
+
if portability == "axi-only":
|
|
261
|
+
click.echo(f" → Suggest: {proposed_board_id}")
|
|
262
|
+
if not click.confirm(
|
|
263
|
+
f"\n Board '{hwh_info.boardpart}' is not in the Board DB.\n"
|
|
264
|
+
f" Treat as {proposed_board_id} (AXI-only, any {device_family} board)?",
|
|
265
|
+
default=True,
|
|
266
|
+
):
|
|
267
|
+
click.echo("Aborted.", err=True)
|
|
268
|
+
raise click.exceptions.Exit(1)
|
|
269
|
+
else:
|
|
270
|
+
click.echo(f" → Suggest: {proposed_board_id}")
|
|
271
|
+
if not click.confirm(
|
|
272
|
+
f"\n Platform will be registered as: {proposed_board_id}/<runtime>\n Continue?",
|
|
273
|
+
default=True,
|
|
274
|
+
):
|
|
275
|
+
click.echo("Aborted.", err=True)
|
|
276
|
+
raise click.exceptions.Exit(1)
|
|
277
|
+
board_id = proposed_board_id
|
|
278
|
+
else:
|
|
279
|
+
click.echo(" → No BOARDPART; cannot look up board")
|
|
280
|
+
proposed_board_id, device_family = _resolve_board_id(hwh_info, db)
|
|
281
|
+
board_id = proposed_board_id
|
|
282
|
+
|
|
283
|
+
# --- Step 4: Interactive inputs ---
|
|
284
|
+
click.echo("")
|
|
285
|
+
|
|
286
|
+
ns_prompt = f"{namespace}/" if namespace else ""
|
|
287
|
+
raw_name = click.prompt(f"Design name [{ns_prompt}]", default=f"{ns_prompt}my-design")
|
|
288
|
+
design_name = raw_name.strip()
|
|
289
|
+
|
|
290
|
+
raw_version = click.prompt("Version", default=semver or "0.1.0")
|
|
291
|
+
version = raw_version.strip()
|
|
292
|
+
|
|
293
|
+
runtime = _prompt_runtime()
|
|
294
|
+
|
|
295
|
+
description = click.prompt("Description", default="", show_default=False).strip() or None
|
|
296
|
+
|
|
297
|
+
raw_tags = tags or click.prompt("Tags (comma-separated, optional)", default="", show_default=False)
|
|
298
|
+
tag_list = [t.strip() for t in raw_tags.split(",") if t.strip()] if raw_tags else []
|
|
299
|
+
|
|
300
|
+
# --- Step 5: Derive paths and build YAML ---
|
|
301
|
+
platform_id = f"{board_id}/{runtime}"
|
|
302
|
+
design_slug = design_name.split("/")[-1] if "/" in design_name else design_name
|
|
303
|
+
platform_dir_name = f"{board_id}-{runtime}"
|
|
304
|
+
|
|
305
|
+
design_ref = f"{design_name}:{version}"
|
|
306
|
+
manifest_interfaces = _interfaces_to_manifest(hwh_info.interfaces)
|
|
307
|
+
|
|
308
|
+
manifest_yaml = _make_manifest_stub(
|
|
309
|
+
runtime=runtime,
|
|
310
|
+
board=board_id,
|
|
311
|
+
device_family=device_family,
|
|
312
|
+
design_ref=design_ref,
|
|
313
|
+
interfaces=manifest_interfaces,
|
|
314
|
+
design_portability=portability,
|
|
315
|
+
)
|
|
316
|
+
index_yaml = _make_index_stub(
|
|
317
|
+
design_name=design_name,
|
|
318
|
+
version=version,
|
|
319
|
+
summary=description,
|
|
320
|
+
tags=tag_list,
|
|
321
|
+
platform_id=platform_id,
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
# --- Step 6: Write (or preview) ---
|
|
325
|
+
out_root = output_dir if output_dir is not None else Path(design_slug)
|
|
326
|
+
manifest_path = out_root / platform_dir_name / "manifest.yaml"
|
|
327
|
+
index_path = out_root / "fabricgate-index.yaml"
|
|
328
|
+
|
|
329
|
+
click.echo("\nGenerated:")
|
|
330
|
+
if dry_run:
|
|
331
|
+
click.echo(f" (dry-run) {index_path}")
|
|
332
|
+
click.echo(f" (dry-run) {manifest_path}")
|
|
333
|
+
click.echo("\n--- fabricgate-index.yaml ---")
|
|
334
|
+
click.echo(index_yaml)
|
|
335
|
+
click.echo(f"--- {platform_dir_name}/manifest.yaml ---")
|
|
336
|
+
click.echo(manifest_yaml)
|
|
337
|
+
return
|
|
338
|
+
|
|
339
|
+
try:
|
|
340
|
+
manifest_path.parent.mkdir(parents=True, exist_ok=True)
|
|
341
|
+
manifest_path.write_text(manifest_yaml, encoding="utf-8")
|
|
342
|
+
index_path.write_text(index_yaml, encoding="utf-8")
|
|
343
|
+
except OSError as exc:
|
|
344
|
+
click.echo(f"Error: Write failed: {exc}", err=True)
|
|
345
|
+
raise click.exceptions.Exit(4) from exc
|
|
346
|
+
|
|
347
|
+
click.echo(f" {index_path} ✓")
|
|
348
|
+
click.echo(f" {manifest_path} ✓")
|
|
349
|
+
click.echo(f"\nNote: Place bitstream files in {out_root / platform_dir_name}/ before publishing.")
|
|
350
|
+
click.echo(f"Run `fabricgate push {out_root}/` to publish.")
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def _prompt_runtime() -> str:
|
|
354
|
+
result: str = click.prompt(
|
|
355
|
+
"Runtime",
|
|
356
|
+
type=click.Choice(["pynq", "linux-fpgamgr", "nanopynq"]),
|
|
357
|
+
default="pynq",
|
|
358
|
+
)
|
|
359
|
+
return result
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""fabricgate deprecate command."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
|
|
9
|
+
from fabricgate.cli.exit_codes import exit_code_for
|
|
10
|
+
from fabricgate.sdk import api as sdk_api
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@click.command(name="deprecate")
|
|
14
|
+
@click.option("--undo", is_flag=True, help="Clear deprecated state instead of setting it.")
|
|
15
|
+
@click.option("--message", default="", help="Deprecation message.")
|
|
16
|
+
@click.option("--successor", default=None, help="Suggested successor design ref.")
|
|
17
|
+
@click.argument("design_ref")
|
|
18
|
+
@click.argument("versions", nargs=-1)
|
|
19
|
+
@click.pass_obj
|
|
20
|
+
def deprecate_cmd(
|
|
21
|
+
obj: dict[str, object],
|
|
22
|
+
undo: bool,
|
|
23
|
+
message: str,
|
|
24
|
+
successor: str | None,
|
|
25
|
+
design_ref: str,
|
|
26
|
+
versions: tuple[str, ...],
|
|
27
|
+
) -> None:
|
|
28
|
+
"""Deprecate or undeprecate one or more versions."""
|
|
29
|
+
reg: str = str(obj["registry"])
|
|
30
|
+
version_list = list(versions) if versions else None
|
|
31
|
+
|
|
32
|
+
if undo and message:
|
|
33
|
+
raise click.UsageError("--message cannot be used with --undo.")
|
|
34
|
+
if undo and successor:
|
|
35
|
+
raise click.UsageError("--successor cannot be used with --undo.")
|
|
36
|
+
if not undo and not message.strip():
|
|
37
|
+
raise click.UsageError("--message is required unless --undo is used.")
|
|
38
|
+
|
|
39
|
+
try:
|
|
40
|
+
if undo:
|
|
41
|
+
results = sdk_api.undeprecate(
|
|
42
|
+
design_ref,
|
|
43
|
+
version_list,
|
|
44
|
+
registry=reg,
|
|
45
|
+
)
|
|
46
|
+
else:
|
|
47
|
+
results = sdk_api.deprecate(
|
|
48
|
+
design_ref,
|
|
49
|
+
version_list,
|
|
50
|
+
message=message,
|
|
51
|
+
successor=successor,
|
|
52
|
+
registry=reg,
|
|
53
|
+
)
|
|
54
|
+
except sdk_api.SDKError as exc:
|
|
55
|
+
click.echo(str(exc), err=True)
|
|
56
|
+
raise click.exceptions.Exit(exit_code_for(exc)) from exc
|
|
57
|
+
|
|
58
|
+
if obj["json"]:
|
|
59
|
+
click.echo(json.dumps([r.model_dump(mode="json") for r in results], indent=2))
|
|
60
|
+
return
|
|
61
|
+
|
|
62
|
+
count = len(results)
|
|
63
|
+
action = "Undeprecating" if undo else "Deprecating"
|
|
64
|
+
if count == 1:
|
|
65
|
+
click.echo(f"\n{action} {results[0].name}:{results[0].version} ...")
|
|
66
|
+
else:
|
|
67
|
+
click.echo(f"\n{action} {count} versions of {results[0].name} ...")
|
|
68
|
+
|
|
69
|
+
marker = "undeprecated" if undo else "deprecated"
|
|
70
|
+
for result in results:
|
|
71
|
+
click.echo(f" ✓ {marker} {result.name}:{result.version}")
|
|
72
|
+
|
|
73
|
+
if not undo:
|
|
74
|
+
if message:
|
|
75
|
+
click.echo(f" Message: {message}")
|
|
76
|
+
if successor:
|
|
77
|
+
click.echo(f" Successor: {successor}")
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""fabricgate diff command — compare metadata between two design versions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import string
|
|
7
|
+
from collections.abc import Iterable
|
|
8
|
+
|
|
9
|
+
import click
|
|
10
|
+
|
|
11
|
+
from fabricgate.cli.exit_codes import exit_code_for
|
|
12
|
+
from fabricgate.models.api.responses import VersionDiffField, VersionDiffResponse
|
|
13
|
+
from fabricgate.sdk import api as sdk_api
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _sanitize_label(value: str) -> str:
|
|
17
|
+
return "".join(ch if ch.isprintable() and ch not in "\r\n\t" else "?" for ch in value)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _sanitize_labels(values: Iterable[str]) -> list[str]:
|
|
21
|
+
return [_sanitize_label(v) for v in values]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _format_size(value: int) -> str:
|
|
25
|
+
size = float(value)
|
|
26
|
+
units = ["B", "KB", "MB", "GB", "TB"]
|
|
27
|
+
for unit in units:
|
|
28
|
+
if size < 1024 or unit == units[-1]:
|
|
29
|
+
if unit == "B":
|
|
30
|
+
return f"{int(size)} {unit}"
|
|
31
|
+
if size.is_integer():
|
|
32
|
+
return f"{int(size)} {unit}"
|
|
33
|
+
return f"{size:.1f} {unit}"
|
|
34
|
+
size /= 1024
|
|
35
|
+
return f"{int(value)} B" # pragma: no cover
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _format_value(field_name: str, value: object) -> str:
|
|
39
|
+
if value is None:
|
|
40
|
+
return "null"
|
|
41
|
+
if isinstance(value, bool):
|
|
42
|
+
return "true" if value else "false"
|
|
43
|
+
if isinstance(value, int):
|
|
44
|
+
if field_name.endswith("size"):
|
|
45
|
+
return _format_size(value)
|
|
46
|
+
return str(value)
|
|
47
|
+
if isinstance(value, float):
|
|
48
|
+
return str(value)
|
|
49
|
+
if isinstance(value, str):
|
|
50
|
+
if field_name.endswith("sha256") and all(char in string.hexdigits for char in value):
|
|
51
|
+
return value
|
|
52
|
+
return json.dumps(value, ensure_ascii=False)
|
|
53
|
+
return json.dumps(value, ensure_ascii=False, separators=(", ", ": "))
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _format_field(field: VersionDiffField, width: int) -> str:
|
|
57
|
+
before = _format_value(field.field, field.base)
|
|
58
|
+
after = _format_value(field.field, field.head)
|
|
59
|
+
safe_field = _sanitize_label(field.field)
|
|
60
|
+
return f" {safe_field.ljust(width)} {before} → {after}"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _format_platforms(platforms: list[str]) -> str:
|
|
64
|
+
return ", ".join(_sanitize_labels(platforms)) if platforms else "(none)"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _render_text(result: VersionDiffResponse, design_ref: str) -> None:
|
|
68
|
+
design_name = design_ref.split(":", 1)[0]
|
|
69
|
+
click.echo(f"Diff: {design_name} {result.base} → {result.head}")
|
|
70
|
+
click.echo()
|
|
71
|
+
click.echo(f" Platforms added: {_format_platforms(result.summary.platforms_added)}")
|
|
72
|
+
click.echo(f" Platforms removed: {_format_platforms(result.summary.platforms_removed)}")
|
|
73
|
+
click.echo(f" Platforms changed: {_format_platforms(result.summary.platforms_changed)}")
|
|
74
|
+
|
|
75
|
+
if not result.diff:
|
|
76
|
+
click.echo()
|
|
77
|
+
click.echo("No differences found.")
|
|
78
|
+
return
|
|
79
|
+
|
|
80
|
+
for entry in result.diff:
|
|
81
|
+
click.echo()
|
|
82
|
+
click.echo(f"{_sanitize_label(entry.platform)}:")
|
|
83
|
+
if entry.fields == "added":
|
|
84
|
+
click.echo(f" (added in {result.head})")
|
|
85
|
+
continue
|
|
86
|
+
if entry.fields == "removed":
|
|
87
|
+
click.echo(f" (removed from {result.base})")
|
|
88
|
+
continue
|
|
89
|
+
|
|
90
|
+
width = max(len(_sanitize_label(field.field)) for field in entry.fields)
|
|
91
|
+
for field in entry.fields:
|
|
92
|
+
click.echo(_format_field(field, width))
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@click.command(name="diff")
|
|
96
|
+
@click.argument("design_ref")
|
|
97
|
+
@click.option("--base", "base_ref", required=True, help="Base version or namespace/design:version.")
|
|
98
|
+
@click.option("--platform", default=None, help="Compare only one platform (device/runtime).")
|
|
99
|
+
@click.pass_obj
|
|
100
|
+
def diff_cmd(
|
|
101
|
+
obj: dict[str, object],
|
|
102
|
+
design_ref: str,
|
|
103
|
+
base_ref: str,
|
|
104
|
+
platform: str | None,
|
|
105
|
+
) -> None:
|
|
106
|
+
"""Show metadata differences between two design versions."""
|
|
107
|
+
reg: str = str(obj["registry"])
|
|
108
|
+
try:
|
|
109
|
+
result = sdk_api.diff(
|
|
110
|
+
design_ref,
|
|
111
|
+
base=base_ref,
|
|
112
|
+
platform=platform,
|
|
113
|
+
registry=reg,
|
|
114
|
+
)
|
|
115
|
+
except sdk_api.SDKError as exc:
|
|
116
|
+
click.echo(str(exc), err=True)
|
|
117
|
+
raise click.exceptions.Exit(exit_code_for(exc)) from exc
|
|
118
|
+
|
|
119
|
+
if obj["json"]:
|
|
120
|
+
click.echo(json.dumps(result.model_dump(mode="json"), indent=2))
|
|
121
|
+
return
|
|
122
|
+
|
|
123
|
+
_render_text(result, design_ref)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""fabricgate info command."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
|
|
9
|
+
from fabricgate.cli.exit_codes import exit_code_for
|
|
10
|
+
from fabricgate.sdk import api as sdk_api
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@click.command(name="info")
|
|
14
|
+
@click.argument("design_ref")
|
|
15
|
+
@click.pass_obj
|
|
16
|
+
def info_cmd(obj: dict[str, object], design_ref: str) -> None:
|
|
17
|
+
"""Show detailed metadata for a design reference."""
|
|
18
|
+
try:
|
|
19
|
+
result = sdk_api.info(design_ref, registry=str(obj["registry"]))
|
|
20
|
+
except sdk_api.SDKError as exc:
|
|
21
|
+
click.echo(str(exc), err=True)
|
|
22
|
+
raise click.exceptions.Exit(exit_code_for(exc)) from exc
|
|
23
|
+
|
|
24
|
+
if obj["json"]:
|
|
25
|
+
click.echo(json.dumps(result.model_dump(mode="json"), indent=2))
|
|
26
|
+
return
|
|
27
|
+
|
|
28
|
+
click.echo(f"Name: {result.name}")
|
|
29
|
+
click.echo(f"Version: {result.version}")
|
|
30
|
+
if result.summary:
|
|
31
|
+
click.echo(f"Summary: {result.summary}")
|
|
32
|
+
if result.license:
|
|
33
|
+
click.echo(f"License: {result.license}")
|
|
34
|
+
if result.author:
|
|
35
|
+
click.echo(f"Author: {result.author}")
|
|
36
|
+
if result.repository:
|
|
37
|
+
click.echo(f"Repository: {result.repository}")
|
|
38
|
+
if result.tags:
|
|
39
|
+
click.echo(f"Tags: {', '.join(result.tags)}")
|
|
40
|
+
click.echo("Platforms:")
|
|
41
|
+
for platform in result.platforms:
|
|
42
|
+
click.echo(f" {platform.platform}")
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""fabricgate license-check command — tool requirements and compatibility check."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
|
|
9
|
+
from fabricgate.cli.exit_codes import exit_code_for
|
|
10
|
+
from fabricgate.sdk import api as sdk_api
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@click.command(name="license-check")
|
|
14
|
+
@click.argument("design_ref")
|
|
15
|
+
@click.option("--tool", "tools", multiple=True, help="Tool identifier to check (repeatable).")
|
|
16
|
+
@click.option("--edition", default=None, help="Edition to check (e.g. standard, enterprise).")
|
|
17
|
+
@click.pass_obj
|
|
18
|
+
def license_check_cmd(
|
|
19
|
+
obj: dict[str, object],
|
|
20
|
+
design_ref: str,
|
|
21
|
+
tools: tuple[str, ...],
|
|
22
|
+
edition: str | None,
|
|
23
|
+
) -> None:
|
|
24
|
+
"""Check tool requirements for a design version."""
|
|
25
|
+
reg: str = str(obj["registry"])
|
|
26
|
+
tool_list = list(tools) if tools else None
|
|
27
|
+
try:
|
|
28
|
+
result = sdk_api.license_check(
|
|
29
|
+
design_ref,
|
|
30
|
+
tool=tool_list,
|
|
31
|
+
edition=edition,
|
|
32
|
+
registry=reg,
|
|
33
|
+
)
|
|
34
|
+
except sdk_api.SDKError as exc:
|
|
35
|
+
click.echo(str(exc), err=True)
|
|
36
|
+
raise click.exceptions.Exit(exit_code_for(exc)) from exc
|
|
37
|
+
|
|
38
|
+
if obj["json"]:
|
|
39
|
+
click.echo(json.dumps(result.model_dump(mode="json"), indent=2))
|
|
40
|
+
return
|
|
41
|
+
|
|
42
|
+
click.echo(f"Design: {result.design}:{result.version}")
|
|
43
|
+
|
|
44
|
+
if not result.tool_requirements:
|
|
45
|
+
click.echo("\nNo tool requirements defined.")
|
|
46
|
+
click.echo(f"\nDisclaimer: {result.disclaimer}")
|
|
47
|
+
# GENERIC: no requirements to check (ADR-008). Exit 2 is reserved for
|
|
48
|
+
# PERMISSION; a missing-data signal must not collide with it.
|
|
49
|
+
raise click.exceptions.Exit(1)
|
|
50
|
+
|
|
51
|
+
if result.check is not None:
|
|
52
|
+
# Tool/edition check mode
|
|
53
|
+
click.echo(f"\ncheck: {result.check.tool}" + (f" ({result.check.edition})" if result.check.edition else ""))
|
|
54
|
+
symbol = "\u2713" if result.check.meets_requirements else "\u2717"
|
|
55
|
+
for note in result.check.notes:
|
|
56
|
+
click.echo(f" {symbol} {note}")
|
|
57
|
+
status = "MEETS REQUIREMENTS" if result.check.meets_requirements else "DOES NOT MEET REQUIREMENTS"
|
|
58
|
+
click.echo(f"\nResult: {status}")
|
|
59
|
+
else:
|
|
60
|
+
# List mode
|
|
61
|
+
click.echo("\nTool Requirements:")
|
|
62
|
+
for req in result.tool_requirements:
|
|
63
|
+
label = "required" if req.required else "optional"
|
|
64
|
+
click.echo(f" {req.tool} ({label})")
|
|
65
|
+
if req.min_version:
|
|
66
|
+
click.echo(f" Min version: {req.min_version}")
|
|
67
|
+
if req.edition:
|
|
68
|
+
click.echo(f" Edition: {req.edition}")
|
|
69
|
+
if req.note:
|
|
70
|
+
click.echo(f" Note: {req.note}")
|
|
71
|
+
click.echo()
|
|
72
|
+
|
|
73
|
+
click.echo(f"Disclaimer: {result.disclaimer}")
|
|
74
|
+
|
|
75
|
+
if result.check is not None and not result.check.meets_requirements:
|
|
76
|
+
raise click.exceptions.Exit(3)
|