Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
381dcb0
refactor(cli): drop direct click usage from the typer app
Emin017 Sep 7, 2026
f46d18b
refactor(cli): unify typer app construction in a shared factory
Emin017 Sep 7, 2026
1375650
test(cli): parse ast in the shared-factory invariant
Emin017 Sep 7, 2026
ddd3293
feat(cli): render command help in markdown mode
Emin017 Sep 7, 2026
b534abb
fix(cli): keep angle-bracket placeholders in markdown help
Emin017 Sep 7, 2026
c6d4ec1
docs(cli): add long markdown help for high-frequency commands
Emin017 Sep 7, 2026
9c76763
docs(cli): fix docstring fidelity and narrow-width help rendering
Emin017 Sep 7, 2026
b202e3c
feat(cli): add ecc doc command for bundled guides
Emin017 Sep 7, 2026
88ad4ba
fix(cli): keep doc output byte-exact and encoding-safe
Emin017 Sep 7, 2026
0921ebd
fix(cli): keep doc section slices byte-exact
Emin017 Sep 7, 2026
4c0a536
docs(guides): document ecc doc in the user guide
Emin017 Sep 7, 2026
a39bc88
docs(guides): add doc to the command overview
Emin017 Sep 7, 2026
6ed6c9a
fix(packaging): collect rich unicode data modules in the bundle
Emin017 Sep 7, 2026
6763bc4
feat(cli): add ecc doc --sections topic index
Emin017 Sep 7, 2026
23096ad
fix(cli): keep heading parsing on one line and pin doc sections contract
Emin017 Sep 7, 2026
fefbb2f
fix(packaging): stop bundling host libfontconfig in the bundle
Emin017 Sep 7, 2026
e85a2a1
refactor(utility): lazy-load plot helpers to keep matplotlib off CLI …
Emin017 Sep 7, 2026
a0dfdba
fix(utility): keep lazy plot exports typed, discoverable, and regress…
Emin017 Sep 7, 2026
cd2063e
refactor(cli): simplify ecc doc to full-guide output with terminal pager
Emin017 Sep 7, 2026
e7f3a98
fix(tools-ecc): keep matplotlib off non-plot CLI paths such as ecc do…
Emin017 Sep 7, 2026
7395fcf
feat(cli): keep rich highlighting inside the ecc doc pager
Emin017 Sep 7, 2026
b4d9e5e
feat(cli): ship the ecc doc guides inside the wheel
Emin017 Sep 7, 2026
b03d722
fix(cli): repair moved guide links and pin doc packaging contracts
Emin017 Sep 7, 2026
9e24109
feat(cli): enable shell completion via --show-completion
Emin017 Sep 7, 2026
1be57d6
docs(cli): collapse exhaustive param listings in the config guide
Emin017 Sep 7, 2026
e13fbd4
test(cli): stabilize help rendering under CI
Emin017 Sep 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/actions/build-pyinstaller-bundle/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -76,4 +76,6 @@ runs:
"$SMOKE_DIR/ecc" --help
"$SMOKE_DIR/ecc" --version
"$SMOKE_DIR/ecc" version --json
"$SMOKE_DIR/ecc" doc config --plain > /dev/null
"$SMOKE_DIR/ecc" doc ug --lang cn --plain > /dev/null
test -x "$SMOKE_DIR/_internal/torch/bin/torch_shm_manager"
48 changes: 20 additions & 28 deletions chipcompiler/cli/app.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import json
import os
from collections.abc import Sequence
from typing import Annotated

import click
import typer

from chipcompiler.cli.commands.doc import register_doc_commands
from chipcompiler.cli.commands.doctor import register_doctor_commands
from chipcompiler.cli.commands.param import param_app
from chipcompiler.cli.commands.pdk import pdk_app
Expand All @@ -14,20 +15,16 @@
from chipcompiler.cli.commands.rpc import rpc_app
from chipcompiler.cli.commands.signoff import signoff_app
from chipcompiler.cli.commands.workspace import workspace_app
from chipcompiler.cli.core.apps import create_app
from chipcompiler.cli.core.version_info import root_version_line, version_payload, version_text
from chipcompiler.cli.inspection.tool_versions import tool_versions

app = typer.Typer(
add_completion=False,
no_args_is_help=True,
rich_markup_mode=None,
help="ECC - EDA toolchain for RTL-to-GDS flows",
)
app = create_app(help="ECC - EDA toolchain for RTL-to-GDS flows", add_completion=True)


def version_callback(value: bool) -> None: # noqa: FBT001 -- typer invokes Option callbacks positionally
if value:
click.echo(root_version_line())
typer.echo(root_version_line())
raise typer.Exit()


Expand Down Expand Up @@ -57,17 +54,17 @@ def version_cmd(
tools = tool_versions()
if jsonl:
for name in ("ecc", "dreamplace", "ecc_tools"):
click.echo(json.dumps({"component": name, "version": payload[name]}))
typer.echo(json.dumps({"component": name, "version": payload[name]}))
for name, version in tools.items():
click.echo(json.dumps({"component": name, "version": version}))
typer.echo(json.dumps({"component": name, "version": version}))
elif json_output:
click.echo(json.dumps({**payload, "tools": tools}))
typer.echo(json.dumps({**payload, "tools": tools}))
elif plain:
from chipcompiler.cli.rendering.render import render_plain

render_plain(({**payload, **tools},))
else:
click.echo(version_text(payload, tools))
typer.echo(version_text(payload, tools))


@app.command("layout-image", help="Render a GDS file into a layout image")
Expand All @@ -80,11 +77,13 @@ def layout_image_cmd(
from chipcompiler.tools.klayout_tool.image import save_snapshot_image

if not save_snapshot_image(gds_file=gds, img_file=image, width=width, height=height):
raise click.ClickException(f"Failed to render layout image from {gds} to {image}")
typer.echo(f"Error: Failed to render layout image from {gds} to {image}", err=True)
raise typer.Exit(1)


register_project_commands(app)
register_doctor_commands(app)
register_doc_commands(app)
app.add_typer(param_app, name="param")
app.add_typer(pdk_app, name="pdk")
app.add_typer(project_app, name="project")
Expand All @@ -95,21 +94,14 @@ def layout_image_cmd(


def invoke_typer_app(argv: Sequence[str]) -> int:
if not argv:
command = typer.main.get_command(app)
click.echo(command.get_help(click.Context(command, info_name="ecc")), err=True)
command = typer.main.get_command(app)
# Shell completion requests carry their arguments in env vars, not argv.
if not argv and "_ECC_COMPLETE" not in os.environ:
typer.echo(command.get_help(typer.Context(command, info_name="ecc")), err=True)
return 1

command = typer.main.get_command(app)
try:
result = command.main(
args=list(argv),
prog_name="ecc",
standalone_mode=False,
)
except click.exceptions.Exit as exc:
return int(exc.exit_code or 0)
except click.ClickException as exc:
exc.show()
return int(exc.exit_code or 1)
return int(result or 0)
command.main(args=list(argv), prog_name="ecc")
except SystemExit as exc:
return int(exc.code or 0)
return 0
77 changes: 77 additions & 0 deletions chipcompiler/cli/commands/doc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
"""Render the bundled CLI guide documents in the terminal."""

import sys
from enum import Enum
from typing import Annotated

import typer

from chipcompiler.cli.core import docs
from chipcompiler.cli.rendering.pretty import supports_color


class DocTopic(str, Enum):
config = "config"
ug = "ug"
tutorial = "tutorial"
dev = "dev"


class DocLanguage(str, Enum):
en = "en"
cn = "cn"


def register_doc_commands(app: typer.Typer) -> None:
app.command("doc", help="Show a bundled guide (config/ug/tutorial/dev) in the terminal")(
doc_cmd
)


def doc_cmd(
topic: Annotated[DocTopic, typer.Argument(help="Guide to show")],
*,
lang: Annotated[DocLanguage, typer.Option("--lang", help="Guide language")] = DocLanguage.en,
plain: Annotated[
bool,
typer.Option("--plain", help="Print the raw markdown instead of the rendered layout"),
] = False,
) -> None:
try:
raw = docs.load_guide(topic.value, lang.value)
if plain:
_write_plain(raw)
return
text = raw.decode("utf-8")
except docs.GuideNotFoundError as exc:
typer.echo(f"Error: {exc}", err=True)
raise typer.Exit(1) from None
except UnicodeDecodeError:
typer.echo(
f"Error: guide is not valid UTF-8: {docs.GUIDE_STEMS[topic.value]}.{lang.value}.md",
err=True,
)
raise typer.Exit(1) from None
except OSError as exc:
typer.echo(f"Error: could not read guide: {exc}", err=True)
raise typer.Exit(1) from None

from chipcompiler.cli.rendering.render import render_markdown

try:
render_markdown(text, color=supports_color(), pager=True)
except UnicodeEncodeError:
typer.echo(
"Error: terminal encoding cannot render this guide; try --plain",
err=True,
)
raise typer.Exit(1) from None


def _write_plain(data: bytes) -> None:
stream = getattr(sys.stdout, "buffer", None)
if stream is None:
sys.stdout.write(data.decode("utf-8"))
else:
stream.write(data)
stream.flush()
89 changes: 74 additions & 15 deletions chipcompiler/cli/commands/param.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from chipcompiler.cli.command_handlers.param import param_set as param_set_handler
from chipcompiler.cli.command_handlers.param import param_show as param_show_handler
from chipcompiler.cli.command_handlers.param import param_unset as param_unset_handler
from chipcompiler.cli.core.apps import create_app
from chipcompiler.cli.core.inputs import (
ParamDiffInput,
ParamListInput,
Expand All @@ -25,12 +26,7 @@
WorkspaceOption,
)

param_app = typer.Typer(
add_completion=False,
no_args_is_help=True,
rich_markup_mode=None,
help="Manage EDA parameters",
)
param_app = create_app(help="Manage EDA parameters")


def _finish_param(
Expand All @@ -41,7 +37,7 @@ def _finish_param(
execute_command("param", command_input, handler, render_key=f"param:{param_command}")


@param_app.command("list", help="List parameter overrides")
@param_app.command("list")
def list_cmd(
*,
project: ProjectOption = None,
Expand All @@ -52,6 +48,20 @@ def list_cmd(
jsonl: JsonlOption = False,
plain: PlainOption = False,
) -> None:
"""List parameter overrides.

By default lists the legacy-semantic parameters (such as
`design.frequency_mhz`, `floorplan.core_util`, `cts.max_fanout`) plus
direct-config fields that already carry an override. Use `--step STEP` to
show the full reviewed schema of one step (field, type, and where each
value is written), or `--all` to show every reviewed field.

```bash
ecc param list --step cts
```

See 'ecc doc config' for the full reference.
"""
command_input = ParamListInput(
output=output_options(json_output=json_output, jsonl=jsonl, plain=plain),
project=project_options(project),
Expand All @@ -62,7 +72,7 @@ def list_cmd(
_finish_param("list", command_input, param_list_handler)


@param_app.command("show", help="Show one parameter value")
@param_app.command("show")
def show_cmd(
*,
key: Annotated[str, typer.Argument()],
Expand All @@ -72,6 +82,15 @@ def show_cmd(
jsonl: JsonlOption = False,
plain: PlainOption = False,
) -> None:
"""Show one parameter value.

Reports the current value, default, source, type, and allowed range,
plus the applicable write targets (`maps_to`, `config_target`,
`pdk_target`). Some parameters (such as `sta.max_paths`) are passed at
runtime instead of written to a generated configuration file.

See 'ecc doc config' for the full reference.
"""
command_input = ParamShowInput(
output=output_options(json_output=json_output, jsonl=jsonl, plain=plain),
project=project_options(project),
Expand All @@ -81,11 +100,7 @@ def show_cmd(
_finish_param("show", command_input, param_show_handler)


@param_app.command(
"set",
help="Set a parameter override",
context_settings={"ignore_unknown_options": True},
)
@param_app.command("set", context_settings={"ignore_unknown_options": True})
def set_cmd(
*,
key: Annotated[str, typer.Argument()],
Expand All @@ -96,6 +111,33 @@ def set_cmd(
jsonl: JsonlOption = False,
plain: PlainOption = False,
) -> None:
"""Set a parameter override.

Scopes:

- project (default): the override is written to `ecc.toml`
(`[params.*]` / `[pdk.overrides]`) and takes effect on the next
fresh `ecc run`.
- `--workspace NAME`: written to `home/params.toml` `[params]`;
the refresh is immediate and the owning step is marked pending.

Values: scalars are parsed per the reviewed schema type; list and object
values are JSON literals and arrays replace the previous value wholesale.
Invalid values fail with `invalid_value` and nothing is written. In the
workspace scope the owning step and its suffix are marked pending and a
later `ecc run --workspace NAME` resumes from that step.

```bash
ecc param set cts.skew_bound 0.05
ecc param set cts.max_buf_tran 0.30
ecc param set cts.routing_layer '[4, 5]'
```

`pdk.*` path parameters are project-scope only; `pdk.root` is set with
`ecc pdk set-root`.

See 'ecc doc config' for the full reference.
"""
command_input = ParamSetInput(
output=output_options(json_output=json_output, jsonl=jsonl, plain=plain),
project=project_options(project),
Expand All @@ -106,7 +148,7 @@ def set_cmd(
_finish_param("set", command_input, param_set_handler)


@param_app.command("unset", help="Remove a parameter override")
@param_app.command("unset")
def unset_cmd(
*,
key: Annotated[str, typer.Argument()],
Expand All @@ -116,6 +158,15 @@ def unset_cmd(
jsonl: JsonlOption = False,
plain: PlainOption = False,
) -> None:
"""Remove a parameter override.

Project scope deletes the key from `ecc.toml`, restoring the default.
Workspace scope (`--workspace NAME`) restores the pre-edit `baseline` and
drops the override record; the owning step and its suffix stay invalid
until they run again.

See 'ecc doc config' for the full reference.
"""
command_input = ParamUnsetInput(
output=output_options(json_output=json_output, jsonl=jsonl, plain=plain),
project=project_options(project),
Expand All @@ -125,7 +176,7 @@ def unset_cmd(
_finish_param("unset", command_input, param_unset_handler)


@param_app.command("diff", help="Compare parameter overrides with defaults")
@param_app.command("diff")
def diff_cmd(
*,
project: ProjectOption = None,
Expand All @@ -134,6 +185,14 @@ def diff_cmd(
jsonl: JsonlOption = False,
plain: PlainOption = False,
) -> None:
"""Compare parameter overrides with defaults.

Project scope lists parameters whose value differs from the default.
Workspace scope (`--workspace NAME`) lists local overrides together with
their baselines.

See 'ecc doc config' for the full reference.
"""
command_input = ParamDiffInput(
output=output_options(json_output=json_output, jsonl=jsonl, plain=plain),
project=project_options(project),
Expand Down
Loading
Loading