From 381dcb07a4e1215a5535610b5d699204b8460df7 Mon Sep 17 00:00:00 2001 From: Emin Date: Mon, 7 Sep 2026 11:20:32 +0800 Subject: [PATCH 01/26] refactor(cli): drop direct click usage from the typer app Typer has vendored click since 0.26, so application code should go through typer's re-exports instead of importing click itself: - click.echo -> typer.echo - the layout-image failure now prints the same "Error: ..." line via typer.echo(err=True) and raises typer.Exit(1) instead of click.ClickException - invoke_typer_app runs the command in standalone mode and converts SystemExit back into an int return code, replacing the non-standalone click exception plumbing; click.Ctrl-C aborts are now handled by click ("Aborted!", exit 1) instead of propagating as a traceback --- chipcompiler/cli/app.py | 34 +++++++++++++--------------------- 1 file changed, 13 insertions(+), 21 deletions(-) diff --git a/chipcompiler/cli/app.py b/chipcompiler/cli/app.py index 79a9b4e3..3cef9a62 100644 --- a/chipcompiler/cli/app.py +++ b/chipcompiler/cli/app.py @@ -2,7 +2,6 @@ from collections.abc import Sequence from typing import Annotated -import click import typer from chipcompiler.cli.commands.doctor import register_doctor_commands @@ -27,7 +26,7 @@ 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() @@ -57,17 +56,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") @@ -80,7 +79,8 @@ 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) @@ -95,21 +95,13 @@ def layout_image_cmd( def invoke_typer_app(argv: Sequence[str]) -> int: + command = typer.main.get_command(app) if not argv: - command = typer.main.get_command(app) - click.echo(command.get_help(click.Context(command, info_name="ecc")), err=True) + 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 From f46d18b5b357461ccafca110bd59b3670313333b Mon Sep 17 00:00:00 2001 From: Emin Date: Mon, 7 Sep 2026 15:14:17 +0800 Subject: [PATCH 02/26] refactor(cli): unify typer app construction in a shared factory --- chipcompiler/cli/app.py | 8 ++------ chipcompiler/cli/commands/param.py | 8 ++------ chipcompiler/cli/commands/pdk.py | 8 ++------ chipcompiler/cli/commands/project_config.py | 8 ++------ chipcompiler/cli/commands/report.py | 8 ++------ chipcompiler/cli/commands/rpc.py | 9 +++------ chipcompiler/cli/commands/signoff.py | 8 ++------ chipcompiler/cli/commands/workspace.py | 8 ++------ chipcompiler/cli/core/apps.py | 10 ++++++++++ test/cli/test_cli_module_layout.py | 11 +++++++++++ 10 files changed, 38 insertions(+), 48 deletions(-) create mode 100644 chipcompiler/cli/core/apps.py diff --git a/chipcompiler/cli/app.py b/chipcompiler/cli/app.py index 3cef9a62..de44b164 100644 --- a/chipcompiler/cli/app.py +++ b/chipcompiler/cli/app.py @@ -13,15 +13,11 @@ 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") def version_callback(value: bool) -> None: # noqa: FBT001 -- typer invokes Option callbacks positionally diff --git a/chipcompiler/cli/commands/param.py b/chipcompiler/cli/commands/param.py index 82695317..486a4890 100644 --- a/chipcompiler/cli/commands/param.py +++ b/chipcompiler/cli/commands/param.py @@ -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, @@ -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( diff --git a/chipcompiler/cli/commands/pdk.py b/chipcompiler/cli/commands/pdk.py index bf7092d7..8ffe4996 100644 --- a/chipcompiler/cli/commands/pdk.py +++ b/chipcompiler/cli/commands/pdk.py @@ -3,6 +3,7 @@ import typer from chipcompiler.cli.command_handlers import pdk as pdk_handlers +from chipcompiler.cli.core.apps import create_app from chipcompiler.cli.core.inputs import ( PdkSetRootInput, PdkSetupInput, @@ -19,12 +20,7 @@ ProjectOption, ) -pdk_app = typer.Typer( - add_completion=False, - no_args_is_help=True, - rich_markup_mode=None, - help="Show and configure the PDK path used by this project", -) +pdk_app = create_app(help="Show and configure the PDK path used by this project") def _finish(subcommand: str, command_input, handler) -> None: diff --git a/chipcompiler/cli/commands/project_config.py b/chipcompiler/cli/commands/project_config.py index 13ae5336..f14c42ff 100644 --- a/chipcompiler/cli/commands/project_config.py +++ b/chipcompiler/cli/commands/project_config.py @@ -5,6 +5,7 @@ import typer from chipcompiler.cli.command_handlers import project_config as handlers +from chipcompiler.cli.core.apps import create_app from chipcompiler.cli.core.inputs import ( ProjectAddInput, ProjectSetInput, @@ -16,12 +17,7 @@ from chipcompiler.cli.core.invocation import execute_command from chipcompiler.cli.core.options import JsonlOption, JsonOption, PlainOption, ProjectOption -project_app = typer.Typer( - add_completion=False, - no_args_is_help=True, - rich_markup_mode=None, - help="Edit project declarations in ecc.toml", -) +project_app = create_app(help="Edit project declarations in ecc.toml") def _finish(subcommand: str, command_input, handler) -> None: diff --git a/chipcompiler/cli/commands/report.py b/chipcompiler/cli/commands/report.py index e3d8525b..d30986d1 100644 --- a/chipcompiler/cli/commands/report.py +++ b/chipcompiler/cli/commands/report.py @@ -3,6 +3,7 @@ import typer from chipcompiler.cli.command_handlers import report as report_handlers +from chipcompiler.cli.core.apps import create_app from chipcompiler.cli.core.inputs import ( ReportChecklistInput, ReportQorInput, @@ -20,12 +21,7 @@ WorkspaceOption, ) -report_app = typer.Typer( - add_completion=False, - no_args_is_help=True, - rich_markup_mode=None, - help="Generate design-summary, QoR score, checklist, and step reports", -) +report_app = create_app(help="Generate design-summary, QoR score, checklist, and step reports") OutputPathOption = Annotated[ str | None, diff --git a/chipcompiler/cli/commands/rpc.py b/chipcompiler/cli/commands/rpc.py index 48b46556..184b9629 100644 --- a/chipcompiler/cli/commands/rpc.py +++ b/chipcompiler/cli/commands/rpc.py @@ -2,12 +2,9 @@ import typer -rpc_app = typer.Typer( - add_completion=False, - no_args_is_help=True, - rich_markup_mode=None, - help="Run the private ECC JSON-RPC runtime", -) +from chipcompiler.cli.core.apps import create_app + +rpc_app = create_app(help="Run the private ECC JSON-RPC runtime") @rpc_app.command("serve", help="Serve the private ECC JSON-RPC runtime") diff --git a/chipcompiler/cli/commands/signoff.py b/chipcompiler/cli/commands/signoff.py index 5a2994a1..3dfcc4f5 100644 --- a/chipcompiler/cli/commands/signoff.py +++ b/chipcompiler/cli/commands/signoff.py @@ -3,6 +3,7 @@ import typer from chipcompiler.cli.command_handlers import signoff as signoff_handlers +from chipcompiler.cli.core.apps import create_app from chipcompiler.cli.core.inputs import ( SignoffExportInput, SignoffInspectInput, @@ -18,12 +19,7 @@ WorkspaceOption, ) -signoff_app = typer.Typer( - add_completion=False, - no_args_is_help=True, - rich_markup_mode=None, - help="Inspect and export signoff packages", -) +signoff_app = create_app(help="Inspect and export signoff packages") def _finish(subcommand: str, command_input, handler) -> None: diff --git a/chipcompiler/cli/commands/workspace.py b/chipcompiler/cli/commands/workspace.py index 1a3030a6..53a1d9da 100644 --- a/chipcompiler/cli/commands/workspace.py +++ b/chipcompiler/cli/commands/workspace.py @@ -5,16 +5,12 @@ import typer from chipcompiler.cli.command_handlers import project as project_handlers +from chipcompiler.cli.core.apps import create_app from chipcompiler.cli.core.inputs import WorkspaceRefreshInput, output_options, project_options from chipcompiler.cli.core.invocation import execute_command from chipcompiler.cli.core.options import JsonlOption, JsonOption, PlainOption, ProjectOption -workspace_app = typer.Typer( - add_completion=False, - no_args_is_help=True, - rich_markup_mode=None, - help="Refresh managed workspaces from project configuration", -) +workspace_app = create_app(help="Refresh managed workspaces from project configuration") @workspace_app.command("refresh", help="Recreate a workspace from ecc.toml without running it") diff --git a/chipcompiler/cli/core/apps.py b/chipcompiler/cli/core/apps.py new file mode 100644 index 00000000..3d41101d --- /dev/null +++ b/chipcompiler/cli/core/apps.py @@ -0,0 +1,10 @@ +import typer + + +def create_app(*, help: str) -> typer.Typer: + return typer.Typer( + add_completion=False, + no_args_is_help=True, + rich_markup_mode=None, + help=help, + ) diff --git a/test/cli/test_cli_module_layout.py b/test/cli/test_cli_module_layout.py index 5339aa6e..d80d9f1f 100644 --- a/test/cli/test_cli_module_layout.py +++ b/test/cli/test_cli_module_layout.py @@ -173,3 +173,14 @@ def test_production_code_does_not_import_removed_inspection_modules(): source = source_path.read_text() for name in forbidden_imports: assert name not in source, source_path + + +def test_typer_apps_are_created_through_the_shared_factory(): + package_root = Path(__file__).parents[2] / "chipcompiler" / "cli" + factory = package_root / "core" / "apps.py" + + for source_path in package_root.rglob("*.py"): + if source_path == factory: + continue + source = source_path.read_text() + assert "typer.Typer(" not in source, source_path From 1375650e811dd952f09ea096c92f36cc39e3a07f Mon Sep 17 00:00:00 2001 From: Emin Date: Mon, 7 Sep 2026 15:18:28 +0800 Subject: [PATCH 03/26] test(cli): parse ast in the shared-factory invariant --- test/cli/test_cli_module_layout.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/test/cli/test_cli_module_layout.py b/test/cli/test_cli_module_layout.py index d80d9f1f..50b08154 100644 --- a/test/cli/test_cli_module_layout.py +++ b/test/cli/test_cli_module_layout.py @@ -1,3 +1,4 @@ +import ast import importlib from pathlib import Path @@ -182,5 +183,12 @@ def test_typer_apps_are_created_through_the_shared_factory(): for source_path in package_root.rglob("*.py"): if source_path == factory: continue - source = source_path.read_text() - assert "typer.Typer(" not in source, source_path + tree = ast.parse(source_path.read_text()) + for node in ast.walk(tree): + if not isinstance(node, ast.Call): + continue + func = node.func + if isinstance(func, ast.Name) and func.id == "Typer": + raise AssertionError(source_path) + if isinstance(func, ast.Attribute) and func.attr == "Typer": + raise AssertionError(source_path) From ddd3293532b3d44843e329ceac4e4d0325ac11fd Mon Sep 17 00:00:00 2001 From: Emin Date: Mon, 7 Sep 2026 15:22:28 +0800 Subject: [PATCH 04/26] feat(cli): render command help in markdown mode --- chipcompiler/cli/core/apps.py | 2 +- test/cli/test_help_rendering.py | 63 +++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 test/cli/test_help_rendering.py diff --git a/chipcompiler/cli/core/apps.py b/chipcompiler/cli/core/apps.py index 3d41101d..76109d68 100644 --- a/chipcompiler/cli/core/apps.py +++ b/chipcompiler/cli/core/apps.py @@ -5,6 +5,6 @@ def create_app(*, help: str) -> typer.Typer: return typer.Typer( add_completion=False, no_args_is_help=True, - rich_markup_mode=None, + rich_markup_mode="markdown", help=help, ) diff --git a/test/cli/test_help_rendering.py b/test/cli/test_help_rendering.py new file mode 100644 index 00000000..458da467 --- /dev/null +++ b/test/cli/test_help_rendering.py @@ -0,0 +1,63 @@ +import pytest +import typer + +from chipcompiler.cli import main as cli_main +from chipcompiler.cli.app import app + + +def _walk(command, path): + yield path + for name, sub in sorted(getattr(command, "commands", {}).items()): + yield from _walk(sub, [*path, name]) + + +def _all_command_paths(): + return list(_walk(typer.main.get_command(app), [])) + + +@pytest.mark.parametrize("path", _all_command_paths(), ids=lambda path: " ".join(path) or "ecc") +def test_help_renders_for_every_command(path, capsys): + rc = cli_main.run([*path, "--help"]) + + captured = capsys.readouterr() + assert rc == 0 + assert captured.out + assert not captured.err + assert "\x1b[" not in captured.out + + +def test_root_command_list_keeps_one_line_summaries(capsys): + rc = cli_main.run(["--help"]) + + out = capsys.readouterr().out + assert rc == 0 + assert "Run the configured RTL-to-GDS flow" in out + assert "Show resolved project or step configuration" in out + + +def test_param_command_list_keeps_one_line_summaries(capsys): + rc = cli_main.run(["param", "--help"]) + + out = capsys.readouterr().out + assert rc == 0 + assert "List parameter overrides" in out + assert "Show one parameter value" in out + assert "Set a parameter override" in out + assert "Remove a parameter override" in out + assert "Compare parameter overrides with defaults" in out + + +def test_workspace_refresh_summary_unchanged(capsys): + rc = cli_main.run(["workspace", "--help"]) + + out = capsys.readouterr().out + assert rc == 0 + assert "Recreate a workspace from ecc.toml without running it" in out + + +def test_pdk_set_root_summary_unchanged(capsys): + rc = cli_main.run(["pdk", "--help"]) + + out = capsys.readouterr().out + assert rc == 0 + assert "Set the [pdk] root path in ecc.toml" in out From b534abb2972b6b993e5d34f70e50687ba6fe4118 Mon Sep 17 00:00:00 2001 From: Emin Date: Mon, 7 Sep 2026 15:31:22 +0800 Subject: [PATCH 05/26] fix(cli): keep angle-bracket placeholders in markdown help --- chipcompiler/cli/commands/report.py | 2 +- test/cli/test_help_rendering.py | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/chipcompiler/cli/commands/report.py b/chipcompiler/cli/commands/report.py index d30986d1..cfa873ad 100644 --- a/chipcompiler/cli/commands/report.py +++ b/chipcompiler/cli/commands/report.py @@ -25,7 +25,7 @@ OutputPathOption = Annotated[ str | None, - typer.Option("--output", "-o", help="Report destination (default: /signoff/)"), + typer.Option("--output", "-o", help="Report destination (default: ``/signoff/)"), ] diff --git a/test/cli/test_help_rendering.py b/test/cli/test_help_rendering.py index 458da467..d054ceaa 100644 --- a/test/cli/test_help_rendering.py +++ b/test/cli/test_help_rendering.py @@ -61,3 +61,11 @@ def test_pdk_set_root_summary_unchanged(capsys): out = capsys.readouterr().out assert rc == 0 assert "Set the [pdk] root path in ecc.toml" in out + + +def test_option_help_preserves_angle_bracket_placeholder(capsys): + rc = cli_main.run(["report", "qor", "--help"]) + + out = capsys.readouterr().out + assert rc == 0 + assert "/signoff/" in out From c6d4ec1b7ab25da2adf51662eefa8133d9eac6f0 Mon Sep 17 00:00:00 2001 From: Emin Date: Mon, 7 Sep 2026 15:46:41 +0800 Subject: [PATCH 06/26] docs(cli): add long markdown help for high-frequency commands --- chipcompiler/cli/commands/param.py | 78 +++++++++++++++++++++++--- chipcompiler/cli/commands/pdk.py | 11 +++- chipcompiler/cli/commands/project.py | 28 ++++++++- chipcompiler/cli/commands/workspace.py | 12 +++- test/cli/test_help_rendering.py | 72 ++++++++++++++++++++++++ 5 files changed, 188 insertions(+), 13 deletions(-) diff --git a/chipcompiler/cli/commands/param.py b/chipcompiler/cli/commands/param.py index 486a4890..346b5a60 100644 --- a/chipcompiler/cli/commands/param.py +++ b/chipcompiler/cli/commands/param.py @@ -37,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, @@ -48,6 +48,21 @@ 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 the JSON + config field each value lands in), 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), @@ -58,7 +73,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()], @@ -68,6 +83,14 @@ 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 write targets (`maps_to`, `config_target`, `pdk_target`) that + show where the value lands in the generated tool configuration. + + 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), @@ -77,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()], @@ -92,6 +111,30 @@ def set_cmd( jsonl: JsonlOption = False, plain: PlainOption = False, ) -> None: + """Set a parameter override. + + | Scope | Written to | Takes effect | + |---|---|---| + | project (default) | `ecc.toml` `[params.*]` / `[pdk.overrides]` | next fresh `ecc run` | + | `--workspace NAME` | `home/params.toml` `[params]` | immediately; step 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), @@ -102,7 +145,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()], @@ -112,6 +155,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), @@ -121,7 +173,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, @@ -130,6 +182,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), diff --git a/chipcompiler/cli/commands/pdk.py b/chipcompiler/cli/commands/pdk.py index 8ffe4996..76cdda1d 100644 --- a/chipcompiler/cli/commands/pdk.py +++ b/chipcompiler/cli/commands/pdk.py @@ -49,7 +49,7 @@ def setup_cmd( _finish("setup", command_input, pdk_handlers.setup) -@pdk_app.command("set-root", help="Set the [pdk] root path in ecc.toml") +@pdk_app.command("set-root") def set_root_cmd( *, path: Annotated[ @@ -61,6 +61,15 @@ def set_root_cmd( jsonl: JsonlOption = False, plain: PlainOption = False, ) -> None: + """Set the [pdk] root path in ecc.toml. + + `pdk.root` is the base directory for the PDK content paths `pdk.tech`, + `pdk.lefs`, `pdk.libs`, and `pdk.mapping_file`. The design-data paths + `pdk.sdc` and `pdk.spef` resolve against the project directory instead. + All six get file-existence validation. + + See 'ecc doc config' for the full reference. + """ command_input = PdkSetRootInput( output=output_options(json_output=json_output, jsonl=jsonl, plain=plain), project=project_options(project), diff --git a/chipcompiler/cli/commands/project.py b/chipcompiler/cli/commands/project.py index e6cbeafd..13abfe2f 100644 --- a/chipcompiler/cli/commands/project.py +++ b/chipcompiler/cli/commands/project.py @@ -28,12 +28,12 @@ def register_project_commands(app: typer.Typer) -> None: app.command("init", help="Create a new ECC project")(init_cmd) app.command("check", help="Validate the current project setup")(check_cmd) - app.command("run", help="Run the configured RTL-to-GDS flow")(run_cmd) + app.command("run")(run_cmd) app.command( "status", help="Show a quick run/step progress summary (full evidence: 'ecc report step')" )(status_cmd) app.command("log", help="Show available logs or step log content")(log_cmd) - app.command("config", help="Show resolved project or step configuration")(config_cmd) + app.command("config")(config_cmd) app.command("migrate", help="Migrate a legacy runs/ project to the manifest layout")( migrate_cmd ) @@ -114,6 +114,21 @@ def run_cmd( ] = None, plain: PlainOption = False, ) -> None: + """Run the configured RTL-to-GDS flow. + + `--set KEY=VALUE` applies a one-off parameter override; it is accepted + only when the run creates a workspace (including `--overwrite`) and is + recorded in `home/cli-param-overrides.json`. On an existing workspace it + fails with `set_requires_fresh_run` — use + `ecc param set KEY VALUE --workspace NAME` instead. Precedence: + `--set` > `ecc.toml` `[params]` > defaults. + + Parameterized fields in `config/*.json` are re-refreshed from + `home/params.toml` and the PDK before every step, so manual edits are + overwritten. Each step reads the previous step's `output/`. + + See 'ecc doc config' for the full reference. + """ command_input = RunInput( output=output_options(json_output=json_output, jsonl=jsonl, plain=plain), project=project_options(project), @@ -192,6 +207,15 @@ def config_cmd( plain: PlainOption = False, workspace: WorkspaceOption = None, ) -> None: + """Show resolved project or step configuration. + + Without STEP: resolved project-level configuration. With STEP: the + configuration files actually in effect for that step. `lec`, `lvs`, + `postroutelec`, and `harden` have no step-specific configuration + (Tcl-driven or reusing `db_ecc.json`). + + See 'ecc doc config' for the full reference. + """ command_input = ConfigInput( output=output_options(json_output=json_output, jsonl=jsonl, plain=plain), project=project_options(project), diff --git a/chipcompiler/cli/commands/workspace.py b/chipcompiler/cli/commands/workspace.py index 53a1d9da..2d5fb2b4 100644 --- a/chipcompiler/cli/commands/workspace.py +++ b/chipcompiler/cli/commands/workspace.py @@ -13,7 +13,7 @@ workspace_app = create_app(help="Refresh managed workspaces from project configuration") -@workspace_app.command("refresh", help="Recreate a workspace from ecc.toml without running it") +@workspace_app.command("refresh") def refresh_cmd( *, workspace: Annotated[str, typer.Argument(help="Declared workspace name")], @@ -22,6 +22,16 @@ def refresh_cmd( jsonl: JsonlOption = False, plain: PlainOption = False, ) -> None: + """Recreate a workspace from ecc.toml without running it. + + Rebuilds the generated configuration from `ecc.toml` and the PDK; manual + edits to `config/*.json` are overwritten. The workspace hub + `home/params.toml` keeps four sections: `[design]`, `[pdk]`, `[flow]`, + and `[params]`. `pdk.*` path changes cannot be applied with + `ecc param set --workspace`; edit `ecc.toml` and refresh instead. + + See 'ecc doc config' for the full reference. + """ command_input = WorkspaceRefreshInput( output=output_options(json_output=json_output, jsonl=jsonl, plain=plain), project=project_options(project), diff --git a/test/cli/test_help_rendering.py b/test/cli/test_help_rendering.py index d054ceaa..0ef9e6b6 100644 --- a/test/cli/test_help_rendering.py +++ b/test/cli/test_help_rendering.py @@ -69,3 +69,75 @@ def test_option_help_preserves_angle_bracket_placeholder(capsys): out = capsys.readouterr().out assert rc == 0 assert "/signoff/" in out + + +def test_param_set_help_documents_scopes_and_value_parsing(capsys): + rc = cli_main.run(["param", "set", "--help"]) + + out = capsys.readouterr().out + assert rc == 0 + assert "ecc.toml" in out + assert "params.toml" in out + assert "'[4, 5]'" in out + assert "skew_bound" in out + assert "max_buf_tran" in out + assert "invalid_value" in out + + +def test_run_help_documents_fresh_run_override_rule(capsys): + rc = cli_main.run(["run", "--help"]) + + out = capsys.readouterr().out + assert rc == 0 + assert "set_requires_fresh_run" in out + assert "cli-param-overrides.json" in out + + +def test_workspace_refresh_help_warns_about_manual_edits(capsys): + rc = cli_main.run(["workspace", "refresh", "--help"]) + + out = capsys.readouterr().out + assert rc == 0 + assert "overwritten" in out + + +def test_config_help_names_steps_without_step_specific_config(capsys): + rc = cli_main.run(["config", "--help"]) + + out = capsys.readouterr().out + assert rc == 0 + for step in ("lec", "lvs", "postroutelec", "harden"): + assert step in out + + +def test_pdk_set_root_help_documents_path_resolution(capsys): + rc = cli_main.run(["pdk", "set-root", "--help"]) + + out = capsys.readouterr().out + assert rc == 0 + assert "pdk.sdc" in out + assert "pdk.spef" in out + assert "project directory" in out + + +@pytest.mark.parametrize( + "path", + [ + ["param", "list"], + ["param", "show"], + ["param", "set"], + ["param", "unset"], + ["param", "diff"], + ["run"], + ["config"], + ["workspace", "refresh"], + ["pdk", "set-root"], + ], + ids=lambda path: " ".join(path), +) +def test_enriched_help_points_to_doc_config(path, capsys): + rc = cli_main.run([*path, "--help"]) + + out = capsys.readouterr().out + assert rc == 0 + assert "ecc doc config" in out From 9c76763d723667ad3aae5de504990c1c0bc62453 Mon Sep 17 00:00:00 2001 From: Emin Date: Mon, 7 Sep 2026 15:53:35 +0800 Subject: [PATCH 07/26] docs(cli): fix docstring fidelity and narrow-width help rendering --- chipcompiler/cli/commands/param.py | 21 ++++++++++++--------- chipcompiler/cli/commands/project.py | 5 +++-- test/cli/test_help_rendering.py | 12 ++++++++++++ 3 files changed, 27 insertions(+), 11 deletions(-) diff --git a/chipcompiler/cli/commands/param.py b/chipcompiler/cli/commands/param.py index 346b5a60..cfd01429 100644 --- a/chipcompiler/cli/commands/param.py +++ b/chipcompiler/cli/commands/param.py @@ -53,9 +53,8 @@ def list_cmd( 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 the JSON - config field each value lands in), or `--all` to show every reviewed - field. + 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 @@ -86,8 +85,9 @@ def show_cmd( """Show one parameter value. Reports the current value, default, source, type, and allowed range, - plus the write targets (`maps_to`, `config_target`, `pdk_target`) that - show where the value lands in the generated tool configuration. + 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. """ @@ -113,10 +113,13 @@ def set_cmd( ) -> None: """Set a parameter override. - | Scope | Written to | Takes effect | - |---|---|---| - | project (default) | `ecc.toml` `[params.*]` / `[pdk.overrides]` | next fresh `ecc run` | - | `--workspace NAME` | `home/params.toml` `[params]` | immediately; step pending | + 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. diff --git a/chipcompiler/cli/commands/project.py b/chipcompiler/cli/commands/project.py index 13abfe2f..6a8c99cc 100644 --- a/chipcompiler/cli/commands/project.py +++ b/chipcompiler/cli/commands/project.py @@ -125,7 +125,8 @@ def run_cmd( Parameterized fields in `config/*.json` are re-refreshed from `home/params.toml` and the PDK before every step, so manual edits are - overwritten. Each step reads the previous step's `output/`. + overwritten. Each step reads the previous step's `output/`; the first + step reads the design's origin verilog/DEF. See 'ecc doc config' for the full reference. """ @@ -212,7 +213,7 @@ def config_cmd( Without STEP: resolved project-level configuration. With STEP: the configuration files actually in effect for that step. `lec`, `lvs`, `postroutelec`, and `harden` have no step-specific configuration - (Tcl-driven or reusing `db_ecc.json`). + (Tcl-driven, tool-default, or reusing `db_ecc.json`). See 'ecc doc config' for the full reference. """ diff --git a/test/cli/test_help_rendering.py b/test/cli/test_help_rendering.py index 0ef9e6b6..fe43ba5d 100644 --- a/test/cli/test_help_rendering.py +++ b/test/cli/test_help_rendering.py @@ -84,6 +84,18 @@ def test_param_set_help_documents_scopes_and_value_parsing(capsys): assert "invalid_value" in out +def test_param_set_help_wraps_at_narrow_width(capsys, monkeypatch): + monkeypatch.setenv("COLUMNS", "50") + + rc = cli_main.run(["param", "set", "--help"]) + + out = capsys.readouterr().out + assert rc == 0 + assert "…" not in out + for token in ("pdk.overrides", "params.toml", "workspace NAME"): + assert token in out + + def test_run_help_documents_fresh_run_override_rule(capsys): rc = cli_main.run(["run", "--help"]) From b202e3c4691f61540a716964a1e9d69de181d17d Mon Sep 17 00:00:00 2001 From: Emin Date: Mon, 7 Sep 2026 16:05:16 +0800 Subject: [PATCH 08/26] feat(cli): add ecc doc command for bundled guides --- chipcompiler/cli/app.py | 2 + chipcompiler/cli/commands/doc.py | 64 +++++++++++++ chipcompiler/cli/core/docs.py | 56 ++++++++++++ chipcompiler/cli/rendering/render.py | 8 ++ ecc.spec | 26 ++++++ test/cli/test_doc.py | 131 +++++++++++++++++++++++++++ 6 files changed, 287 insertions(+) create mode 100644 chipcompiler/cli/commands/doc.py create mode 100644 chipcompiler/cli/core/docs.py create mode 100644 test/cli/test_doc.py diff --git a/chipcompiler/cli/app.py b/chipcompiler/cli/app.py index de44b164..bc228d90 100644 --- a/chipcompiler/cli/app.py +++ b/chipcompiler/cli/app.py @@ -4,6 +4,7 @@ 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 @@ -81,6 +82,7 @@ def layout_image_cmd( 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") diff --git a/chipcompiler/cli/commands/doc.py b/chipcompiler/cli/commands/doc.py new file mode 100644 index 00000000..8440230c --- /dev/null +++ b/chipcompiler/cli/commands/doc.py @@ -0,0 +1,64 @@ +"""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")], + section: Annotated[ + str | None, + typer.Argument(help="Section token from the guide's numbered headings, e.g. 7 or 8.5"), + ] = None, + *, + 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: + text = docs.load_guide(topic.value, lang.value) + if section is not None: + text = docs.slice_section(text, section) + except docs.GuideNotFoundError as exc: + typer.echo(f"Error: {exc}", err=True) + raise typer.Exit(1) from None + except docs.SectionNotFoundError as exc: + typer.echo(f"Error: {exc}", err=True) + raise typer.Exit(1) from None + except (OSError, UnicodeDecodeError) as exc: + typer.echo(f"Error: could not read guide: {exc}", err=True) + raise typer.Exit(1) from None + + if plain: + sys.stdout.write(text) + return + + from chipcompiler.cli.rendering.render import render_markdown + + render_markdown(text, color=supports_color()) diff --git a/chipcompiler/cli/core/docs.py b/chipcompiler/cli/core/docs.py new file mode 100644 index 00000000..3ca54859 --- /dev/null +++ b/chipcompiler/cli/core/docs.py @@ -0,0 +1,56 @@ +"""Locate, slice, and load the bundled CLI guide documents.""" + +import re +import sys +from pathlib import Path + +GUIDE_STEMS = { + "config": "ecc-cli-config", + "ug": "ecc-cli-ug", + "tutorial": "ecc-cli-tutorial", + "dev": "ecc-cli-dev", +} + +_NUMBERED_HEADING = re.compile(r"^## (?P\d+(?:\.\d+)*)\.", re.MULTILINE) +_ANY_HEADING = re.compile(r"^## ", re.MULTILINE) + + +class GuideNotFoundError(FileNotFoundError): + pass + + +class SectionNotFoundError(LookupError): + pass + + +def guides_root() -> Path: + bundle_root = getattr(sys, "_MEIPASS", None) + if bundle_root: + return Path(bundle_root) / "docs" + import chipcompiler + + return Path(chipcompiler.__file__).resolve().parent.parent / "docs" + + +def load_guide(topic: str, lang: str) -> str: + name = f"{GUIDE_STEMS[topic]}.{lang}.md" + path = guides_root() / name + if not path.is_file(): + raise GuideNotFoundError(f"doc resource not found: {name}") + return path.read_text(encoding="utf-8") + + +def heading_tokens(text: str) -> list[str]: + return [match.group("token") for match in _NUMBERED_HEADING.finditer(text)] + + +def slice_section(text: str, token: str) -> str: + for match in _NUMBERED_HEADING.finditer(text): + if match.group("token") != token: + continue + start = match.start() + next_match = _ANY_HEADING.search(text, match.end()) + end = next_match.start() if next_match else len(text) + return text[start:end].rstrip() + "\n" + available = " ".join(heading_tokens(text)) + raise SectionNotFoundError(f"no section {token}; available sections: {available}") diff --git a/chipcompiler/cli/rendering/render.py b/chipcompiler/cli/rendering/render.py index eaeda96a..37344695 100644 --- a/chipcompiler/cli/rendering/render.py +++ b/chipcompiler/cli/rendering/render.py @@ -50,6 +50,14 @@ def _plain_value(value) -> str: return s +def render_markdown(text: str, file=None, *, color: bool) -> None: + from rich.console import Console + from rich.markdown import Markdown + + console = Console(file=file or sys.stdout, force_terminal=True, no_color=not color) + console.print(Markdown(text)) + + def render_result( result: CommandResult, mode: OutputMode, file=None, command=None, *, color=True ) -> None: diff --git a/ecc.spec b/ecc.spec index 2f530cb4..3f9b9c81 100644 --- a/ecc.spec +++ b/ecc.spec @@ -52,6 +52,17 @@ DREAMPLACE_THIRDPARTY_FILES = ( "thirdparty/NCTUgr.ICCAD2012/ICCAD12.set", ) +DOC_GUIDES = ( + "docs/ecc-cli-config.en.md", + "docs/ecc-cli-config.cn.md", + "docs/ecc-cli-ug.en.md", + "docs/ecc-cli-ug.cn.md", + "docs/ecc-cli-tutorial.en.md", + "docs/ecc-cli-tutorial.cn.md", + "docs/ecc-cli-dev.en.md", + "docs/ecc-cli-dev.cn.md", +) + LINUX_RUNTIME_LIBS = ( "/lib/x86_64-linux-gnu/libgomp.so.1", "/lib/x86_64-linux-gnu/libtbb.so.12", @@ -174,6 +185,20 @@ def collect_dreamplace_thirdparty_files(): return datas +def collect_doc_guides(): + datas = [] + for relpath in DOC_GUIDES: + src = ECC_DIR / relpath + if src.is_file(): + datas.append((str(src), "docs")) + else: + warnings.warn( + f"Required ECC runtime resource was not collected: {relpath}", + stacklevel=2, + ) + return datas + + def collect_platform_runtime_libs(): if sys.platform.startswith("linux"): binaries = [] @@ -221,6 +246,7 @@ datas.extend(collect_required_metadata()) datas.extend(collect_ecc_resources()) datas.extend(collect_jsonrpcserver_resources()) datas.extend(collect_dreamplace_thirdparty_files()) +datas.extend(collect_doc_guides()) binaries = [] binaries.extend(ecc_binaries) diff --git a/test/cli/test_doc.py b/test/cli/test_doc.py new file mode 100644 index 00000000..5388c38e --- /dev/null +++ b/test/cli/test_doc.py @@ -0,0 +1,131 @@ +import sys +from pathlib import Path + +import pytest + +from chipcompiler.cli import main as cli_main +from chipcompiler.cli.core import docs +from chipcompiler.cli.core.docs import guides_root + + +def test_guides_root_points_at_repository_docs_in_dev_mode(): + repo_docs = Path(__file__).parents[2] / "docs" + + assert guides_root() == repo_docs + assert (guides_root() / "ecc-cli-config.en.md").is_file() + + +def test_all_four_topics_resolve_in_both_languages(): + for topic in docs.GUIDE_STEMS: + for lang in ("en", "cn"): + text = docs.load_guide(topic, lang) + assert text.startswith("# ") + + +def test_doc_config_plain_is_byte_identical_to_the_guide_file(capsys): + rc = cli_main.run(["doc", "config", "--plain"]) + + out = capsys.readouterr().out + assert rc == 0 + assert out.encode("utf-8") == (guides_root() / "ecc-cli-config.en.md").read_bytes() + + +def test_doc_uses_packaged_docs_when_frozen(tmp_path, monkeypatch, capsys): + guide = tmp_path / "docs" / "ecc-cli-config.en.md" + guide.parent.mkdir() + guide.write_text("# Packaged guide\n", encoding="utf-8") + monkeypatch.setattr(sys, "_MEIPASS", str(tmp_path), raising=False) + + rc = cli_main.run(["doc", "config", "--plain"]) + + out = capsys.readouterr().out + assert rc == 0 + assert out == "# Packaged guide\n" + + +def test_missing_guide_resource_fails_with_exit_1(tmp_path, monkeypatch, capsys): + monkeypatch.setattr(sys, "_MEIPASS", str(tmp_path), raising=False) + + rc = cli_main.run(["doc", "config", "--plain"]) + + captured = capsys.readouterr() + assert rc == 1 + assert "Error: doc resource not found: ecc-cli-config.en.md" in captured.err + + +def test_doc_section_slice_selects_exactly_one_section(capsys): + rc = cli_main.run(["doc", "config", "7", "--plain"]) + + out = capsys.readouterr().out + assert rc == 0 + assert "cts_ecc.json" in out + assert out.startswith("## 7.") + assert "## 8." not in out + + +def test_doc_section_tokens_match_exactly(capsys): + rc = cli_main.run(["doc", "ug", "8", "--plain"]) + + out = capsys.readouterr().out + assert rc == 0 + assert out.startswith("## 8. config") + assert "## 8.5." not in out + + rc = cli_main.run(["doc", "ug", "8.5", "--plain"]) + + out = capsys.readouterr().out + assert rc == 0 + assert out.startswith("## 8.5.") + + +def test_doc_unknown_section_lists_available_tokens(capsys): + rc = cli_main.run(["doc", "config", "99"]) + + captured = capsys.readouterr() + assert rc == 1 + assert "no section 99" in captured.err + for token in ("0", "7", "16"): + assert token in captured.err + + +def test_doc_without_section_shows_the_full_guide(capsys): + rc = cli_main.run(["doc", "tutorial", "1", "--plain"]) + + out = capsys.readouterr().out + assert rc == 0 + assert out.startswith("## 1.") + + +def test_doc_chinese_language(capsys): + rc = cli_main.run(["doc", "config", "--lang", "cn", "--plain"]) + + out = capsys.readouterr().out + assert rc == 0 + assert any("\u4e00" <= ch <= "\u9fff" for ch in out) + + +def test_doc_default_text_output_keeps_unicode_layout_without_ansi(capsys): + rc = cli_main.run(["doc", "config", "1"]) + + out = capsys.readouterr().out + assert rc == 0 + assert "\x1b[" not in out + assert "\u2500" in out + + +@pytest.mark.parametrize( + "argv", + [ + ["doc", "bogus"], + ["doc", "CONFIG"], + ["doc"], + ["doc", "config", "--lang", "jp"], + ], + ids=lambda argv: " ".join(argv), +) +def test_doc_invalid_arguments_exit_2(argv, capsys): + rc = cli_main.run(argv) + + captured = capsys.readouterr() + assert rc == 2 + assert captured.err From 88ad4ba7177c8d59a04f301abe6b896daffb8310 Mon Sep 17 00:00:00 2001 From: Emin Date: Mon, 7 Sep 2026 16:15:41 +0800 Subject: [PATCH 09/26] fix(cli): keep doc output byte-exact and encoding-safe --- chipcompiler/cli/commands/doc.py | 34 +++++++++++++++++++--- chipcompiler/cli/core/docs.py | 4 +-- test/cli/test_doc.py | 48 +++++++++++++++++++++++++++++--- 3 files changed, 76 insertions(+), 10 deletions(-) diff --git a/chipcompiler/cli/commands/doc.py b/chipcompiler/cli/commands/doc.py index 8440230c..efa4881e 100644 --- a/chipcompiler/cli/commands/doc.py +++ b/chipcompiler/cli/commands/doc.py @@ -42,7 +42,11 @@ def doc_cmd( ] = False, ) -> None: try: - text = docs.load_guide(topic.value, lang.value) + raw = docs.load_guide(topic.value, lang.value) + if plain and section is None: + _write_plain(raw) + return + text = raw.decode("utf-8") if section is not None: text = docs.slice_section(text, section) except docs.GuideNotFoundError as exc: @@ -51,14 +55,36 @@ def doc_cmd( except docs.SectionNotFoundError as exc: typer.echo(f"Error: {exc}", err=True) raise typer.Exit(1) from None - except (OSError, UnicodeDecodeError) as exc: + 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 if plain: - sys.stdout.write(text) + _write_plain(text.encode("utf-8")) return from chipcompiler.cli.rendering.render import render_markdown - render_markdown(text, color=supports_color()) + try: + render_markdown(text, color=supports_color()) + 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() diff --git a/chipcompiler/cli/core/docs.py b/chipcompiler/cli/core/docs.py index 3ca54859..0f7d1a18 100644 --- a/chipcompiler/cli/core/docs.py +++ b/chipcompiler/cli/core/docs.py @@ -32,12 +32,12 @@ def guides_root() -> Path: return Path(chipcompiler.__file__).resolve().parent.parent / "docs" -def load_guide(topic: str, lang: str) -> str: +def load_guide(topic: str, lang: str) -> bytes: name = f"{GUIDE_STEMS[topic]}.{lang}.md" path = guides_root() / name if not path.is_file(): raise GuideNotFoundError(f"doc resource not found: {name}") - return path.read_text(encoding="utf-8") + return path.read_bytes() def heading_tokens(text: str) -> list[str]: diff --git a/test/cli/test_doc.py b/test/cli/test_doc.py index 5388c38e..45546f7e 100644 --- a/test/cli/test_doc.py +++ b/test/cli/test_doc.py @@ -18,16 +18,56 @@ def test_guides_root_points_at_repository_docs_in_dev_mode(): def test_all_four_topics_resolve_in_both_languages(): for topic in docs.GUIDE_STEMS: for lang in ("en", "cn"): - text = docs.load_guide(topic, lang) + text = docs.load_guide(topic, lang).decode("utf-8") assert text.startswith("# ") -def test_doc_config_plain_is_byte_identical_to_the_guide_file(capsys): +def test_doc_config_plain_is_byte_identical_to_the_guide_file(capsysbinary): rc = cli_main.run(["doc", "config", "--plain"]) - out = capsys.readouterr().out + out = capsysbinary.readouterr().out assert rc == 0 - assert out.encode("utf-8") == (guides_root() / "ecc-cli-config.en.md").read_bytes() + assert out == (guides_root() / "ecc-cli-config.en.md").read_bytes() + + +def test_doc_config_section_plain_output_is_byte_exact(capsysbinary): + rc = cli_main.run(["doc", "config", "7", "--plain"]) + + out = capsysbinary.readouterr().out + assert rc == 0 + assert out in (guides_root() / "ecc-cli-config.en.md").read_bytes() + + +def test_doc_plain_preserves_crlf_line_endings(tmp_path, monkeypatch, capsysbinary): + guide = tmp_path / "docs" / "ecc-cli-config.en.md" + guide.parent.mkdir() + guide.write_bytes(b"# Packaged guide\r\n\r\ntext\r\n") + monkeypatch.setattr(sys, "_MEIPASS", str(tmp_path), raising=False) + + rc = cli_main.run(["doc", "config", "--plain"]) + + out = capsysbinary.readouterr().out + assert rc == 0 + assert out == b"# Packaged guide\r\n\r\ntext\r\n" + + +def test_rendered_output_survives_non_utf8_stdout_via_plain_fallback(tmp_path, monkeypatch, capsys): + import io + + guide = tmp_path / "docs" / "ecc-cli-config.en.md" + guide.parent.mkdir() + guide.write_bytes("# Packaged guide\n\ntext with ünïcode\n".encode()) + monkeypatch.setattr(sys, "_MEIPASS", str(tmp_path), raising=False) + ascii_stdout = io.TextIOWrapper(io.BytesIO(), encoding="ascii") + monkeypatch.setattr(sys, "stdout", ascii_stdout) + + rc = cli_main.run(["doc", "config", "--plain"]) + assert rc == 0 + + rc = cli_main.run(["doc", "config"]) + captured = capsys.readouterr() + assert rc == 1 + assert "Error:" in captured.err def test_doc_uses_packaged_docs_when_frozen(tmp_path, monkeypatch, capsys): From 0921ebd5e44b783486dc7eb633b4a558e12d2237 Mon Sep 17 00:00:00 2001 From: Emin Date: Mon, 7 Sep 2026 16:20:06 +0800 Subject: [PATCH 10/26] fix(cli): keep doc section slices byte-exact --- chipcompiler/cli/core/docs.py | 2 +- test/cli/test_doc.py | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/chipcompiler/cli/core/docs.py b/chipcompiler/cli/core/docs.py index 0f7d1a18..1b455661 100644 --- a/chipcompiler/cli/core/docs.py +++ b/chipcompiler/cli/core/docs.py @@ -51,6 +51,6 @@ def slice_section(text: str, token: str) -> str: start = match.start() next_match = _ANY_HEADING.search(text, match.end()) end = next_match.start() if next_match else len(text) - return text[start:end].rstrip() + "\n" + return text[start:end] available = " ".join(heading_tokens(text)) raise SectionNotFoundError(f"no section {token}; available sections: {available}") diff --git a/test/cli/test_doc.py b/test/cli/test_doc.py index 45546f7e..c2e6b165 100644 --- a/test/cli/test_doc.py +++ b/test/cli/test_doc.py @@ -51,6 +51,23 @@ def test_doc_plain_preserves_crlf_line_endings(tmp_path, monkeypatch, capsysbina assert out == b"# Packaged guide\r\n\r\ntext\r\n" +def test_doc_section_plain_preserves_crlf_and_trailing_whitespace( + tmp_path, monkeypatch, capsysbinary +): + guide = tmp_path / "docs" / "ecc-cli-config.en.md" + guide.parent.mkdir() + guide.write_bytes( + b"# Guide\r\n\r\n## 1. First\r\n\r\ntext \r\n\r\n\r\n## 2. Second\r\nbody\r\n" + ) + monkeypatch.setattr(sys, "_MEIPASS", str(tmp_path), raising=False) + + rc = cli_main.run(["doc", "config", "1", "--plain"]) + + out = capsysbinary.readouterr().out + assert rc == 0 + assert out == b"## 1. First\r\n\r\ntext \r\n\r\n\r\n" + + def test_rendered_output_survives_non_utf8_stdout_via_plain_fallback(tmp_path, monkeypatch, capsys): import io From 4c0a536a8813aaf4bb6bfcda66b6552f81879708 Mon Sep 17 00:00:00 2001 From: Emin Date: Mon, 7 Sep 2026 16:23:07 +0800 Subject: [PATCH 11/26] docs(guides): document ecc doc in the user guide --- docs/ecc-cli-ug.cn.md | 16 ++++++++++++++++ docs/ecc-cli-ug.en.md | 16 ++++++++++++++++ test/cli/test_doc.py | 7 +++++++ 3 files changed, 39 insertions(+) diff --git a/docs/ecc-cli-ug.cn.md b/docs/ecc-cli-ug.cn.md index 77b14b84..2b911725 100644 --- a/docs/ecc-cli-ug.cn.md +++ b/docs/ecc-cli-ug.cn.md @@ -113,6 +113,22 @@ Commands: rpc Run the private ECC JSON-RPC runtime ``` +## 1.5. doc — 在终端阅读内置指南 + +`ecc doc` 将随包内置的 CLI 指南直接渲染到终端,因此在打包安装(无源码、无文档目录)的环境中也能离线查阅完整参考。 + +```bash +ecc doc config # 完整配置参考(渲染输出) +ecc doc config 7 # 仅第 7 节(cts) +ecc doc ug 8.5 --lang cn # 小数节按编号精确匹配(8 ≠ 8.5) +ecc doc config --plain # 原始 markdown,逐字节输出 +``` + +- 主题:`config`、`ug`、`tutorial`、`dev`;`--lang` 选择 `en`(默认)或 `cn`。 +- `section` 与指南的 `## N.` 编号标题精确匹配;未知编号退出码为 1,并列出全部可用编号。 +- 非法的主题/语言取值由参数校验拒绝(退出码 2)。 +- 管道输出保留 unicode 渲染版式;`--plain` 原样输出原始 markdown,适合脚本处理。 + ## 2. version — 查看版本 ```bash diff --git a/docs/ecc-cli-ug.en.md b/docs/ecc-cli-ug.en.md index 1c866a52..fe15ea06 100644 --- a/docs/ecc-cli-ug.en.md +++ b/docs/ecc-cli-ug.en.md @@ -113,6 +113,22 @@ Commands: rpc Run the private ECC JSON-RPC runtime ``` +## 1.5. doc — read the bundled guides in the terminal + +`ecc doc` renders the bundled CLI guides directly in the terminal, so the full reference stays available offline inside packaged installations. + +```bash +ecc doc config # full configuration reference (rendered) +ecc doc config 7 # only section 7 (cts) +ecc doc ug 8.5 --lang cn # decimal sections are addressed exactly (8 ≠ 8.5) +ecc doc config --plain # raw markdown, byte-for-byte +``` + +- Topics: `config`, `ug`, `tutorial`, `dev`; `--lang` selects `en` (default) or `cn`. +- `section` matches the guides' numbered `## N.` headings exactly; an unknown section exits 1 and lists the available section numbers. +- Invalid topic/language values are rejected by argument validation (exit 2). +- Default output keeps the rendered unicode layout even when piped; `--plain` prints the raw markdown unchanged (script-friendly). + ## 2. version — show versions ```bash diff --git a/test/cli/test_doc.py b/test/cli/test_doc.py index c2e6b165..0719a7d6 100644 --- a/test/cli/test_doc.py +++ b/test/cli/test_doc.py @@ -22,6 +22,13 @@ def test_all_four_topics_resolve_in_both_languages(): assert text.startswith("# ") +def test_ug_guide_documents_the_doc_command_in_both_languages(): + for lang in ("en", "cn"): + text = docs.load_guide("ug", lang).decode("utf-8") + assert "## 1.5. doc" in text + assert "ecc doc config" in text + + def test_doc_config_plain_is_byte_identical_to_the_guide_file(capsysbinary): rc = cli_main.run(["doc", "config", "--plain"]) From a39bc881b524a31bde6c7ad64653435d4501c77a Mon Sep 17 00:00:00 2001 From: Emin Date: Mon, 7 Sep 2026 16:28:13 +0800 Subject: [PATCH 12/26] docs(guides): add doc to the command overview --- docs/ecc-cli-ug.cn.md | 1 + docs/ecc-cli-ug.en.md | 1 + 2 files changed, 2 insertions(+) diff --git a/docs/ecc-cli-ug.cn.md b/docs/ecc-cli-ug.cn.md index 2b911725..38a2c30f 100644 --- a/docs/ecc-cli-ug.cn.md +++ b/docs/ecc-cli-ug.cn.md @@ -104,6 +104,7 @@ Commands: config Show resolved project or step configuration migrate Migrate a legacy runs/ project to the manifest layout doctor Check host environment: PDK, tools, and components + doc Show a bundled guide (config/ug/tutorial/dev) in the terminal param Manage EDA parameters pdk Show and configure the PDK path used by this project project Edit project declarations in ecc.toml diff --git a/docs/ecc-cli-ug.en.md b/docs/ecc-cli-ug.en.md index fe15ea06..e4fa7c72 100644 --- a/docs/ecc-cli-ug.en.md +++ b/docs/ecc-cli-ug.en.md @@ -104,6 +104,7 @@ Commands: config Show resolved project or step configuration migrate Migrate a legacy runs/ project to the manifest layout doctor Check host environment: PDK, tools, and components + doc Show a bundled guide (config/ug/tutorial/dev) in the terminal param Manage EDA parameters pdk Show and configure the PDK path used by this project project Edit project declarations in ecc.toml From 6ed6c9a28e2dfc1024879a71c1294ce772ca5698 Mon Sep 17 00:00:00 2001 From: Emin Date: Mon, 7 Sep 2026 16:39:59 +0800 Subject: [PATCH 13/26] fix(packaging): collect rich unicode data modules in the bundle --- ecc.spec | 19 +++++++++++++++++++ test/packaging/test_cli_entrypoint.py | 21 +++++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/ecc.spec b/ecc.spec index 3f9b9c81..69dd3145 100644 --- a/ecc.spec +++ b/ecc.spec @@ -230,6 +230,24 @@ def collect_ecc_tools_extension_binaries(): ) +def rich_unicode_data_hiddenimports(): + # rich imports its per-Unicode-version cell-width tables dynamically + # (rich._unicode_data.unicode--), so PyInstaller's static + # analysis cannot see them; render-only CJK output needs them at runtime. + module_spec = find_spec("rich") + if module_spec is None or module_spec.submodule_search_locations is None: + return [] + + names = [] + for package_root in module_spec.submodule_search_locations: + data_dir = Path(package_root) / "_unicode_data" + if data_dir.is_dir(): + names.extend( + f"rich._unicode_data.{path.stem}" for path in sorted(data_dir.glob("unicode*.py")) + ) + return names + + ecc_datas, ecc_binaries, ecc_hiddenimports = collect_all("chipcompiler") ecc_tools_datas, ecc_tools_binaries, ecc_tools_hiddenimports = collect_all("ecc_tools_bin") klayout_datas, klayout_binaries, klayout_hiddenimports = collect_all("klayout") @@ -260,6 +278,7 @@ binaries = filter_collected_payloads(binaries) hiddenimports = [] hiddenimports.extend(HIDDENIMPORTS) +hiddenimports.extend(rich_unicode_data_hiddenimports()) hiddenimports.extend(ecc_hiddenimports) hiddenimports.extend(ecc_tools_hiddenimports) hiddenimports.extend(klayout_hiddenimports) diff --git a/test/packaging/test_cli_entrypoint.py b/test/packaging/test_cli_entrypoint.py index 6da80f69..c4aa3872 100644 --- a/test/packaging/test_cli_entrypoint.py +++ b/test/packaging/test_cli_entrypoint.py @@ -34,3 +34,24 @@ def test_pyinstaller_spec_filters_payloads_before_analysis(self): assert datas_filter_index < analysis_index assert binaries_filter_index < analysis_index + + def test_pyinstaller_spec_collects_doc_guides(self): + project_root = os.path.dirname(os.path.dirname(os.path.dirname(__file__))) + spec_path = os.path.join(project_root, "ecc.spec") + + with open(spec_path, encoding="utf-8") as f: + source = f.read() + + assert "datas.extend(collect_doc_guides())" in source + for stem in ("config", "ug", "tutorial", "dev"): + for lang in ("en", "cn"): + assert f"docs/ecc-cli-{stem}.{lang}.md" in source + + def test_pyinstaller_spec_collects_rich_unicode_data_modules(self): + project_root = os.path.dirname(os.path.dirname(os.path.dirname(__file__))) + spec_path = os.path.join(project_root, "ecc.spec") + + with open(spec_path, encoding="utf-8") as f: + source = f.read() + + assert "hiddenimports.extend(rich_unicode_data_hiddenimports())" in source From 6763bc4d6fbcd432abfae770ee4b1d31769a99ae Mon Sep 17 00:00:00 2001 From: Emin Date: Mon, 7 Sep 2026 16:53:35 +0800 Subject: [PATCH 14/26] feat(cli): add ecc doc --sections topic index --- chipcompiler/cli/commands/doc.py | 22 +++++++++++++++++++++- chipcompiler/cli/core/docs.py | 23 +++++++++++++++++++---- docs/ecc-cli-ug.cn.md | 1 + docs/ecc-cli-ug.en.md | 1 + test/cli/test_doc.py | 28 ++++++++++++++++++++++++++++ 5 files changed, 70 insertions(+), 5 deletions(-) diff --git a/chipcompiler/cli/commands/doc.py b/chipcompiler/cli/commands/doc.py index efa4881e..4be444bd 100644 --- a/chipcompiler/cli/commands/doc.py +++ b/chipcompiler/cli/commands/doc.py @@ -36,6 +36,12 @@ def doc_cmd( ] = None, *, lang: Annotated[DocLanguage, typer.Option("--lang", help="Guide language")] = DocLanguage.en, + sections: Annotated[ + bool, + typer.Option( + "--sections", help="List the guide's numbered sections with their titles, then exit" + ), + ] = False, plain: Annotated[ bool, typer.Option("--plain", help="Print the raw markdown instead of the rendered layout"), @@ -43,10 +49,16 @@ def doc_cmd( ) -> None: try: raw = docs.load_guide(topic.value, lang.value) - if plain and section is None: + if plain and section is None and not sections: _write_plain(raw) return text = raw.decode("utf-8") + if sections: + if section is not None: + typer.echo("Error: --sections cannot be combined with SECTION", err=True) + raise typer.Exit(1) + _write_text(docs.table_of_contents(text)) + return if section is not None: text = docs.slice_section(text, section) except docs.GuideNotFoundError as exc: @@ -88,3 +100,11 @@ def _write_plain(data: bytes) -> None: else: stream.write(data) stream.flush() + + +def _write_text(text: str) -> None: + try: + sys.stdout.write(text) + except UnicodeEncodeError: + typer.echo("Error: terminal encoding cannot render this output", err=True) + raise typer.Exit(1) from None diff --git a/chipcompiler/cli/core/docs.py b/chipcompiler/cli/core/docs.py index 1b455661..dfd8a1d9 100644 --- a/chipcompiler/cli/core/docs.py +++ b/chipcompiler/cli/core/docs.py @@ -11,7 +11,7 @@ "dev": "ecc-cli-dev", } -_NUMBERED_HEADING = re.compile(r"^## (?P\d+(?:\.\d+)*)\.", re.MULTILINE) +_NUMBERED_HEADING = re.compile(r"^## (?P\d+(?:\.\d+)*)\.\s*(?P.*)$", re.MULTILINE) _ANY_HEADING = re.compile(r"^## ", re.MULTILINE) @@ -40,8 +40,21 @@ def load_guide(topic: str, lang: str) -> bytes: return path.read_bytes() +def heading_entries(text: str) -> list[tuple[str, str]]: + return [ + (match.group("token"), match.group("title").strip()) + for match in _NUMBERED_HEADING.finditer(text) + ] + + def heading_tokens(text: str) -> list[str]: - return [match.group("token") for match in _NUMBERED_HEADING.finditer(text)] + return [token for token, _ in heading_entries(text)] + + +def table_of_contents(text: str) -> str: + entries = heading_entries(text) + width = max(len(token) for token, _ in entries) + return "".join(f" {token:<{width}} {title}\n" for token, title in entries) def slice_section(text: str, token: str) -> str: @@ -52,5 +65,7 @@ def slice_section(text: str, token: str) -> str: next_match = _ANY_HEADING.search(text, match.end()) end = next_match.start() if next_match else len(text) return text[start:end] - available = " ".join(heading_tokens(text)) - raise SectionNotFoundError(f"no section {token}; available sections: {available}") + entries = heading_entries(text) + width = max(len(token) for token, _ in entries) + listing = "\n".join(f" {token:<{width}} {title}" for token, title in entries) + raise SectionNotFoundError(f"no section {token}; available sections:\n{listing}") diff --git a/docs/ecc-cli-ug.cn.md b/docs/ecc-cli-ug.cn.md index 38a2c30f..da33463a 100644 --- a/docs/ecc-cli-ug.cn.md +++ b/docs/ecc-cli-ug.cn.md @@ -120,6 +120,7 @@ Commands: ```bash ecc doc config # 完整配置参考(渲染输出) +ecc doc config --sections # 列出章节编号与标题 ecc doc config 7 # 仅第 7 节(cts) ecc doc ug 8.5 --lang cn # 小数节按编号精确匹配(8 ≠ 8.5) ecc doc config --plain # 原始 markdown,逐字节输出 diff --git a/docs/ecc-cli-ug.en.md b/docs/ecc-cli-ug.en.md index e4fa7c72..8f725d3f 100644 --- a/docs/ecc-cli-ug.en.md +++ b/docs/ecc-cli-ug.en.md @@ -120,6 +120,7 @@ Commands: ```bash ecc doc config # full configuration reference (rendered) +ecc doc config --sections # list the numbered sections with their titles ecc doc config 7 # only section 7 (cts) ecc doc ug 8.5 --lang cn # decimal sections are addressed exactly (8 ≠ 8.5) ecc doc config --plain # raw markdown, byte-for-byte diff --git a/test/cli/test_doc.py b/test/cli/test_doc.py index 0719a7d6..87077093 100644 --- a/test/cli/test_doc.py +++ b/test/cli/test_doc.py @@ -142,6 +142,33 @@ def test_doc_section_tokens_match_exactly(capsys): assert out.startswith("## 8.5.") +def test_doc_sections_lists_tokens_with_titles(capsys): + rc = cli_main.run(["doc", "config", "--sections"]) + + out = capsys.readouterr().out + assert rc == 0 + assert "Configuration System Overview" in out + assert "cts (ecc-tools)" in out + assert "8.5" not in out + + +def test_doc_sections_with_chinese_titles(capsys): + rc = cli_main.run(["doc", "ug", "--lang", "cn", "--sections"]) + + out = capsys.readouterr().out + assert rc == 0 + assert "8.5" in out + assert any("\u4e00" <= ch <= "\u9fff" for ch in out) + + +def test_doc_sections_rejects_section_combination(capsys): + rc = cli_main.run(["doc", "config", "7", "--sections"]) + + captured = capsys.readouterr() + assert rc == 1 + assert "Error:" in captured.err + + def test_doc_unknown_section_lists_available_tokens(capsys): rc = cli_main.run(["doc", "config", "99"]) @@ -150,6 +177,7 @@ def test_doc_unknown_section_lists_available_tokens(capsys): assert "no section 99" in captured.err for token in ("0", "7", "16"): assert token in captured.err + assert "Configuration System Overview" in captured.err def test_doc_without_section_shows_the_full_guide(capsys): From 23096adf58d8a6a1bbbfdaa8ef70f454a0e7ebb6 Mon Sep 17 00:00:00 2001 From: Emin <me@emin.chat> Date: Mon, 7 Sep 2026 17:08:52 +0800 Subject: [PATCH 15/26] fix(cli): keep heading parsing on one line and pin doc sections contract --- chipcompiler/cli/core/docs.py | 2 +- test/cli/test_doc.py | 49 +++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/chipcompiler/cli/core/docs.py b/chipcompiler/cli/core/docs.py index dfd8a1d9..fafa6f6a 100644 --- a/chipcompiler/cli/core/docs.py +++ b/chipcompiler/cli/core/docs.py @@ -11,7 +11,7 @@ "dev": "ecc-cli-dev", } -_NUMBERED_HEADING = re.compile(r"^## (?P<token>\d+(?:\.\d+)*)\.\s*(?P<title>.*)$", re.MULTILINE) +_NUMBERED_HEADING = re.compile(r"^## (?P<token>\d+(?:\.\d+)*)\.[ \t]*(?P<title>.*)$", re.MULTILINE) _ANY_HEADING = re.compile(r"^## ", re.MULTILINE) diff --git a/test/cli/test_doc.py b/test/cli/test_doc.py index 87077093..38f3a1af 100644 --- a/test/cli/test_doc.py +++ b/test/cli/test_doc.py @@ -152,6 +152,55 @@ def test_doc_sections_lists_tokens_with_titles(capsys): assert "8.5" not in out +def test_heading_parser_handles_empty_titles_and_adjacent_headings(): + text = "## 1.\n## 2. Next\nbody\n" + + assert docs.heading_entries(text) == [("1", ""), ("2", "Next")] + assert docs.slice_section(text, "2") == "## 2. Next\nbody\n" + assert " 2 Next" in docs.table_of_contents(text) + + +def test_doc_sections_output_pairs_each_token_with_its_title(capsys): + rc = cli_main.run(["doc", "ug", "--sections"]) + + out = capsys.readouterr().out + entries = {} + for line in out.splitlines(): + parts = line.strip().split(None, 1) + if parts: + entries[parts[0]] = parts[1] if len(parts) > 1 else "" + assert rc == 0 + assert entries["8"] == "config — view the resolved configuration" + assert entries["8.5"] == ( + "project / workspace — edit project declarations and refresh workspaces" + ) + + +def test_doc_sections_combination_error_is_single_line_with_empty_stdout(capsys): + rc = cli_main.run(["doc", "config", "7", "--sections"]) + + captured = capsys.readouterr() + assert rc == 1 + assert captured.out == "" + assert len(captured.err.strip().splitlines()) == 1 + + +def test_doc_sections_fails_cleanly_on_non_utf8_stdout(tmp_path, monkeypatch, capsys): + import io + + guide = tmp_path / "docs" / "ecc-cli-ug.en.md" + guide.parent.mkdir() + guide.write_bytes("## 1. ünïcode title\n".encode()) + monkeypatch.setattr(sys, "_MEIPASS", str(tmp_path), raising=False) + monkeypatch.setattr(sys, "stdout", io.TextIOWrapper(io.BytesIO(), encoding="ascii")) + + rc = cli_main.run(["doc", "ug", "--sections"]) + + captured = capsys.readouterr() + assert rc == 1 + assert "Error:" in captured.err + + def test_doc_sections_with_chinese_titles(capsys): rc = cli_main.run(["doc", "ug", "--lang", "cn", "--sections"]) From fefbb2fef097c3d820b462e3ffadaf74c6f8d74e Mon Sep 17 00:00:00 2001 From: Emin <me@emin.chat> Date: Mon, 7 Sep 2026 17:59:11 +0800 Subject: [PATCH 16/26] fix(packaging): stop bundling host libfontconfig in the bundle --- ecc.spec | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/ecc.spec b/ecc.spec index 69dd3145..c986db8f 100644 --- a/ecc.spec +++ b/ecc.spec @@ -230,6 +230,22 @@ def collect_ecc_tools_extension_binaries(): ) +def filter_host_fontconfig(binaries): + # PyInstaller pulls libfontconfig in as a DT_NEEDED dependency of the + # bundled libcairo. Do not ship it: a bundled library parses the host's + # /etc/fonts/conf.d, and version skew against those host configs spams + # Fontconfig warnings (and breaks the host fc-list binary with symbol + # errors when the bundle lib dir is on its library search path). The + # host libfontconfig always matches the host fontconfig data. Applied + # to the Analysis output, because input-list filtering cannot stop the + # dependency walk from re-collecting it. + return [ + entry + for entry in binaries + if not any(Path(part).name.startswith("libfontconfig.so") for part in entry[:2]) + ] + + def rich_unicode_data_hiddenimports(): # rich imports its per-Unicode-version cell-width tables dynamically # (rich._unicode_data.unicode<N>-<N>-<N>), so PyInstaller's static @@ -299,6 +315,8 @@ a = Analysis( noarchive=False, ) +a.binaries = filter_host_fontconfig(a.binaries) + pyz = PYZ(a.pure, a.zipped_data) if BUNDLE_MODE == "onedir": From e85a2a1b15639196618d8fb708c8d88a71334cd8 Mon Sep 17 00:00:00 2001 From: Emin <me@emin.chat> Date: Mon, 7 Sep 2026 20:47:36 +0800 Subject: [PATCH 17/26] refactor(utility): lazy-load plot helpers to keep matplotlib off CLI startup chipcompiler.utility imported matplotlib.pyplot at package import time, so every 'ecc' invocation (even --help) paid ~0.8s of matplotlib import and, in the PyInstaller bundle with a cold font cache, spawned fc-list against the host fontconfig config. Re-export the five plot helpers via a PEP 562 module __getattr__ so matplotlib is only imported when a plot function is actually used. Call sites (already function-level imports in tools/ecc/plot.py) are unchanged. --- chipcompiler/utility/__init__.py | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/chipcompiler/utility/__init__.py b/chipcompiler/utility/__init__.py index 3f7a3b88..3e637393 100644 --- a/chipcompiler/utility/__init__.py +++ b/chipcompiler/utility/__init__.py @@ -1,3 +1,5 @@ +from typing import TYPE_CHECKING + from .csv import csv_write from .file import chmod_folder, file_digest, find_files from .filelist import ( @@ -16,9 +18,32 @@ redirect_stdio_to_file, rotate_log_on_start, ) -from .plot import plot_bar_chart, plot_csv_bar_chart, plot_csv_map, plot_csv_table, plot_metrics from .util import track_process_memory +# Plot helpers pull in matplotlib (~1s import, font scan on cold cache), so +# they are re-exported lazily via PEP 562 instead of at package import time. +_PLOT_EXPORTS = frozenset( + {"plot_bar_chart", "plot_csv_bar_chart", "plot_csv_map", "plot_csv_table", "plot_metrics"} +) + +if TYPE_CHECKING: + from .plot import ( + plot_bar_chart, + plot_csv_bar_chart, + plot_csv_map, + plot_csv_table, + plot_metrics, + ) + + +def __getattr__(name: str): + if name in _PLOT_EXPORTS: + from . import plot + + return getattr(plot, name) + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") + + __all__ = [ "chmod_folder", "json_read", From a0dfdbad6c30aa04219ad99df1afdd9315fa0f17 Mon Sep 17 00:00:00 2001 From: Emin <me@emin.chat> Date: Mon, 7 Sep 2026 21:03:03 +0800 Subject: [PATCH 18/26] fix(utility): keep lazy plot exports typed, discoverable, and regression-tested Address codex review of ee9df808: - Move the PEP 562 __getattr__ behind a TYPE_CHECKING/else branch so pyright keeps rejecting unknown chipcompiler.utility attributes instead of accepting them via the inferred Any return. - Add __dir__ so the five lazy plot names stay in dir()/help()/completion without importing matplotlib. - Add fresh-process regression tests asserting that importing chipcompiler.utility and rendering 'ecc --help' leave matplotlib unloaded, and that the plot exports resolve on demand. --- chipcompiler/utility/__init__.py | 15 +++++--- test/utility/test_plot_lazy.py | 64 ++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+), 5 deletions(-) create mode 100644 test/utility/test_plot_lazy.py diff --git a/chipcompiler/utility/__init__.py b/chipcompiler/utility/__init__.py index 3e637393..8f0b288e 100644 --- a/chipcompiler/utility/__init__.py +++ b/chipcompiler/utility/__init__.py @@ -36,12 +36,17 @@ ) -def __getattr__(name: str): - if name in _PLOT_EXPORTS: - from . import plot +else: + # Hidden from type checkers so unknown attributes still fail statically. + def __getattr__(name: str): + if name in _PLOT_EXPORTS: + from . import plot - return getattr(plot, name) - raise AttributeError(f"module {__name__!r} has no attribute {name!r}") + return getattr(plot, name) + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") + + def __dir__() -> list[str]: + return sorted(set(globals()) | _PLOT_EXPORTS) __all__ = [ diff --git a/test/utility/test_plot_lazy.py b/test/utility/test_plot_lazy.py new file mode 100644 index 00000000..318a3d8a --- /dev/null +++ b/test/utility/test_plot_lazy.py @@ -0,0 +1,64 @@ +"""Regression tests: matplotlib must stay off the CLI/package import path. + +chipcompiler.utility re-exports the plot helpers lazily (PEP 562) so that +importing the package — and therefore every ecc command — does not pay for +matplotlib's import or its cold-cache fc-list font scan. +""" + +import subprocess +import sys + + +def _run_fresh(code: str) -> subprocess.CompletedProcess: + return subprocess.run( + [sys.executable, "-c", code], + capture_output=True, + text=True, + check=True, + ) + + +def test_utility_import_does_not_load_matplotlib(): + result = _run_fresh("import sys, chipcompiler.utility; print('matplotlib' in sys.modules)") + assert result.stdout.strip() == "False" + + +def test_cli_help_does_not_load_matplotlib(): + result = _run_fresh( + "import sys; from chipcompiler.cli.main import run; " + "rc = run(['--help']); " + "print(rc, 'matplotlib' in sys.modules)" + ) + assert result.stdout.strip().splitlines()[-1] == "0 False" + + +def test_plot_exports_resolve_lazily(): + result = _run_fresh( + "import sys, chipcompiler.utility as u; " + "names = ['plot_bar_chart', 'plot_csv_bar_chart', 'plot_csv_map', " + "'plot_csv_table', 'plot_metrics']; " + "funcs = [getattr(u, n) for n in names]; " + "print(all(callable(f) for f in funcs), 'matplotlib' in sys.modules)" + ) + assert result.stdout.strip() == "True True" + + +def test_unknown_attribute_still_raises(): + result = _run_fresh( + "import chipcompiler.utility as u\n" + "try:\n" + " u.no_such_name\n" + "except AttributeError:\n" + " print('AttributeError')\n" + ) + assert result.stdout.strip() == "AttributeError" + + +def test_plot_exports_stay_discoverable(): + result = _run_fresh( + "import chipcompiler.utility as u; " + "print(all(n in dir(u) and n in u.__all__ for n in " + "['plot_bar_chart', 'plot_csv_bar_chart', 'plot_csv_map', " + "'plot_csv_table', 'plot_metrics']))" + ) + assert result.stdout.strip() == "True" From cd2063e4a19ce173469d02bdacd5d7111ce5a461 Mon Sep 17 00:00:00 2001 From: Emin <me@emin.chat> Date: Mon, 7 Sep 2026 21:20:17 +0800 Subject: [PATCH 19/26] refactor(cli): simplify ecc doc to full-guide output with terminal pager Drop the SECTION argument and --sections topic index: ecc doc now always shows the whole guide. Rendered output opens in the pydoc pager ($PAGER, falling back to less/more) on a terminal and prints in full when piped; --plain keeps its byte-exact output unpaged for scripts and agents. Also stop forcing terminal mode on colorless streams in render_markdown: rich 15 emits ANSI escapes when force_terminal=True even with no_color set, so piped rendered output leaked escape codes. --- chipcompiler/cli/commands/doc.py | 37 +----- chipcompiler/cli/core/docs.py | 41 +------ chipcompiler/cli/rendering/render.py | 14 ++- docs/ecc-cli-ug.cn.md | 6 +- docs/ecc-cli-ug.en.md | 6 +- test/cli/test_doc.py | 163 +++------------------------ 6 files changed, 35 insertions(+), 232 deletions(-) diff --git a/chipcompiler/cli/commands/doc.py b/chipcompiler/cli/commands/doc.py index 4be444bd..6fc6d1e9 100644 --- a/chipcompiler/cli/commands/doc.py +++ b/chipcompiler/cli/commands/doc.py @@ -30,18 +30,8 @@ def register_doc_commands(app: typer.Typer) -> None: def doc_cmd( topic: Annotated[DocTopic, typer.Argument(help="Guide to show")], - section: Annotated[ - str | None, - typer.Argument(help="Section token from the guide's numbered headings, e.g. 7 or 8.5"), - ] = None, *, lang: Annotated[DocLanguage, typer.Option("--lang", help="Guide language")] = DocLanguage.en, - sections: Annotated[ - bool, - typer.Option( - "--sections", help="List the guide's numbered sections with their titles, then exit" - ), - ] = False, plain: Annotated[ bool, typer.Option("--plain", help="Print the raw markdown instead of the rendered layout"), @@ -49,24 +39,13 @@ def doc_cmd( ) -> None: try: raw = docs.load_guide(topic.value, lang.value) - if plain and section is None and not sections: + if plain: _write_plain(raw) return text = raw.decode("utf-8") - if sections: - if section is not None: - typer.echo("Error: --sections cannot be combined with SECTION", err=True) - raise typer.Exit(1) - _write_text(docs.table_of_contents(text)) - return - if section is not None: - text = docs.slice_section(text, section) except docs.GuideNotFoundError as exc: typer.echo(f"Error: {exc}", err=True) raise typer.Exit(1) from None - except docs.SectionNotFoundError 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", @@ -77,14 +56,10 @@ def doc_cmd( typer.echo(f"Error: could not read guide: {exc}", err=True) raise typer.Exit(1) from None - if plain: - _write_plain(text.encode("utf-8")) - return - from chipcompiler.cli.rendering.render import render_markdown try: - render_markdown(text, color=supports_color()) + render_markdown(text, color=supports_color(), pager=True) except UnicodeEncodeError: typer.echo( "Error: terminal encoding cannot render this guide; try --plain", @@ -100,11 +75,3 @@ def _write_plain(data: bytes) -> None: else: stream.write(data) stream.flush() - - -def _write_text(text: str) -> None: - try: - sys.stdout.write(text) - except UnicodeEncodeError: - typer.echo("Error: terminal encoding cannot render this output", err=True) - raise typer.Exit(1) from None diff --git a/chipcompiler/cli/core/docs.py b/chipcompiler/cli/core/docs.py index fafa6f6a..d6e9202b 100644 --- a/chipcompiler/cli/core/docs.py +++ b/chipcompiler/cli/core/docs.py @@ -1,6 +1,5 @@ -"""Locate, slice, and load the bundled CLI guide documents.""" +"""Locate and load the bundled CLI guide documents.""" -import re import sys from pathlib import Path @@ -11,18 +10,11 @@ "dev": "ecc-cli-dev", } -_NUMBERED_HEADING = re.compile(r"^## (?P<token>\d+(?:\.\d+)*)\.[ \t]*(?P<title>.*)$", re.MULTILINE) -_ANY_HEADING = re.compile(r"^## ", re.MULTILINE) - class GuideNotFoundError(FileNotFoundError): pass -class SectionNotFoundError(LookupError): - pass - - def guides_root() -> Path: bundle_root = getattr(sys, "_MEIPASS", None) if bundle_root: @@ -38,34 +30,3 @@ def load_guide(topic: str, lang: str) -> bytes: if not path.is_file(): raise GuideNotFoundError(f"doc resource not found: {name}") return path.read_bytes() - - -def heading_entries(text: str) -> list[tuple[str, str]]: - return [ - (match.group("token"), match.group("title").strip()) - for match in _NUMBERED_HEADING.finditer(text) - ] - - -def heading_tokens(text: str) -> list[str]: - return [token for token, _ in heading_entries(text)] - - -def table_of_contents(text: str) -> str: - entries = heading_entries(text) - width = max(len(token) for token, _ in entries) - return "".join(f" {token:<{width}} {title}\n" for token, title in entries) - - -def slice_section(text: str, token: str) -> str: - for match in _NUMBERED_HEADING.finditer(text): - if match.group("token") != token: - continue - start = match.start() - next_match = _ANY_HEADING.search(text, match.end()) - end = next_match.start() if next_match else len(text) - return text[start:end] - entries = heading_entries(text) - width = max(len(token) for token, _ in entries) - listing = "\n".join(f" {token:<{width}} {title}" for token, title in entries) - raise SectionNotFoundError(f"no section {token}; available sections:\n{listing}") diff --git a/chipcompiler/cli/rendering/render.py b/chipcompiler/cli/rendering/render.py index 37344695..50fa0b1e 100644 --- a/chipcompiler/cli/rendering/render.py +++ b/chipcompiler/cli/rendering/render.py @@ -50,12 +50,20 @@ def _plain_value(value) -> str: return s -def render_markdown(text: str, file=None, *, color: bool) -> None: +def render_markdown(text: str, file=None, *, color: bool, pager: bool = False) -> None: from rich.console import Console from rich.markdown import Markdown - console = Console(file=file or sys.stdout, force_terminal=True, no_color=not color) - console.print(Markdown(text)) + # force_terminal tracks color: forcing a terminal on a colorless stream + # makes rich 15 emit ANSI escapes even with no_color=True. + console = Console(file=file or sys.stdout, force_terminal=color, no_color=not color) + # Page only on a real terminal: pydoc picks its pager at import time, so + # its own isatty check cannot be trusted once the process has been piped. + if pager and file is None and sys.stdout.isatty(): + with console.pager(): + console.print(Markdown(text)) + else: + console.print(Markdown(text)) def render_result( diff --git a/docs/ecc-cli-ug.cn.md b/docs/ecc-cli-ug.cn.md index da33463a..cbc464f1 100644 --- a/docs/ecc-cli-ug.cn.md +++ b/docs/ecc-cli-ug.cn.md @@ -120,14 +120,12 @@ Commands: ```bash ecc doc config # 完整配置参考(渲染输出) -ecc doc config --sections # 列出章节编号与标题 -ecc doc config 7 # 仅第 7 节(cts) -ecc doc ug 8.5 --lang cn # 小数节按编号精确匹配(8 ≠ 8.5) +ecc doc ug --lang cn # 本指南的中文版 ecc doc config --plain # 原始 markdown,逐字节输出 ``` - 主题:`config`、`ug`、`tutorial`、`dev`;`--lang` 选择 `en`(默认)或 `cn`。 -- `section` 与指南的 `## N.` 编号标题精确匹配;未知编号退出码为 1,并列出全部可用编号。 +- 终端下渲染输出会进入分页器(`$PAGER`,回退到 `less`/`more`)翻阅;管道场景直接全量输出。 - 非法的主题/语言取值由参数校验拒绝(退出码 2)。 - 管道输出保留 unicode 渲染版式;`--plain` 原样输出原始 markdown,适合脚本处理。 diff --git a/docs/ecc-cli-ug.en.md b/docs/ecc-cli-ug.en.md index 8f725d3f..7959e303 100644 --- a/docs/ecc-cli-ug.en.md +++ b/docs/ecc-cli-ug.en.md @@ -120,14 +120,12 @@ Commands: ```bash ecc doc config # full configuration reference (rendered) -ecc doc config --sections # list the numbered sections with their titles -ecc doc config 7 # only section 7 (cts) -ecc doc ug 8.5 --lang cn # decimal sections are addressed exactly (8 ≠ 8.5) +ecc doc ug --lang cn # this guide, Chinese edition ecc doc config --plain # raw markdown, byte-for-byte ``` - Topics: `config`, `ug`, `tutorial`, `dev`; `--lang` selects `en` (default) or `cn`. -- `section` matches the guides' numbered `## N.` headings exactly; an unknown section exits 1 and lists the available section numbers. +- On a terminal the rendered guide opens in a pager (`$PAGER`, falling back to `less`/`more`); when piped it prints in full. - Invalid topic/language values are rejected by argument validation (exit 2). - Default output keeps the rendered unicode layout even when piped; `--plain` prints the raw markdown unchanged (script-friendly). diff --git a/test/cli/test_doc.py b/test/cli/test_doc.py index 38f3a1af..d599f890 100644 --- a/test/cli/test_doc.py +++ b/test/cli/test_doc.py @@ -37,14 +37,6 @@ def test_doc_config_plain_is_byte_identical_to_the_guide_file(capsysbinary): assert out == (guides_root() / "ecc-cli-config.en.md").read_bytes() -def test_doc_config_section_plain_output_is_byte_exact(capsysbinary): - rc = cli_main.run(["doc", "config", "7", "--plain"]) - - out = capsysbinary.readouterr().out - assert rc == 0 - assert out in (guides_root() / "ecc-cli-config.en.md").read_bytes() - - def test_doc_plain_preserves_crlf_line_endings(tmp_path, monkeypatch, capsysbinary): guide = tmp_path / "docs" / "ecc-cli-config.en.md" guide.parent.mkdir() @@ -58,23 +50,6 @@ def test_doc_plain_preserves_crlf_line_endings(tmp_path, monkeypatch, capsysbina assert out == b"# Packaged guide\r\n\r\ntext\r\n" -def test_doc_section_plain_preserves_crlf_and_trailing_whitespace( - tmp_path, monkeypatch, capsysbinary -): - guide = tmp_path / "docs" / "ecc-cli-config.en.md" - guide.parent.mkdir() - guide.write_bytes( - b"# Guide\r\n\r\n## 1. First\r\n\r\ntext \r\n\r\n\r\n## 2. Second\r\nbody\r\n" - ) - monkeypatch.setattr(sys, "_MEIPASS", str(tmp_path), raising=False) - - rc = cli_main.run(["doc", "config", "1", "--plain"]) - - out = capsysbinary.readouterr().out - assert rc == 0 - assert out == b"## 1. First\r\n\r\ntext \r\n\r\n\r\n" - - def test_rendered_output_survives_non_utf8_stdout_via_plain_fallback(tmp_path, monkeypatch, capsys): import io @@ -117,141 +92,36 @@ def test_missing_guide_resource_fails_with_exit_1(tmp_path, monkeypatch, capsys) assert "Error: doc resource not found: ecc-cli-config.en.md" in captured.err -def test_doc_section_slice_selects_exactly_one_section(capsys): - rc = cli_main.run(["doc", "config", "7", "--plain"]) - - out = capsys.readouterr().out - assert rc == 0 - assert "cts_ecc.json" in out - assert out.startswith("## 7.") - assert "## 8." not in out - - -def test_doc_section_tokens_match_exactly(capsys): - rc = cli_main.run(["doc", "ug", "8", "--plain"]) - - out = capsys.readouterr().out - assert rc == 0 - assert out.startswith("## 8. config") - assert "## 8.5." not in out - - rc = cli_main.run(["doc", "ug", "8.5", "--plain"]) - - out = capsys.readouterr().out - assert rc == 0 - assert out.startswith("## 8.5.") - - -def test_doc_sections_lists_tokens_with_titles(capsys): - rc = cli_main.run(["doc", "config", "--sections"]) - - out = capsys.readouterr().out - assert rc == 0 - assert "Configuration System Overview" in out - assert "cts (ecc-tools)" in out - assert "8.5" not in out - - -def test_heading_parser_handles_empty_titles_and_adjacent_headings(): - text = "## 1.\n## 2. Next\nbody\n" - - assert docs.heading_entries(text) == [("1", ""), ("2", "Next")] - assert docs.slice_section(text, "2") == "## 2. Next\nbody\n" - assert " 2 Next" in docs.table_of_contents(text) - - -def test_doc_sections_output_pairs_each_token_with_its_title(capsys): - rc = cli_main.run(["doc", "ug", "--sections"]) - - out = capsys.readouterr().out - entries = {} - for line in out.splitlines(): - parts = line.strip().split(None, 1) - if parts: - entries[parts[0]] = parts[1] if len(parts) > 1 else "" - assert rc == 0 - assert entries["8"] == "config — view the resolved configuration" - assert entries["8.5"] == ( - "project / workspace — edit project declarations and refresh workspaces" - ) - - -def test_doc_sections_combination_error_is_single_line_with_empty_stdout(capsys): - rc = cli_main.run(["doc", "config", "7", "--sections"]) - - captured = capsys.readouterr() - assert rc == 1 - assert captured.out == "" - assert len(captured.err.strip().splitlines()) == 1 - - -def test_doc_sections_fails_cleanly_on_non_utf8_stdout(tmp_path, monkeypatch, capsys): - import io - - guide = tmp_path / "docs" / "ecc-cli-ug.en.md" - guide.parent.mkdir() - guide.write_bytes("## 1. ünïcode title\n".encode()) - monkeypatch.setattr(sys, "_MEIPASS", str(tmp_path), raising=False) - monkeypatch.setattr(sys, "stdout", io.TextIOWrapper(io.BytesIO(), encoding="ascii")) - - rc = cli_main.run(["doc", "ug", "--sections"]) - - captured = capsys.readouterr() - assert rc == 1 - assert "Error:" in captured.err - - -def test_doc_sections_with_chinese_titles(capsys): - rc = cli_main.run(["doc", "ug", "--lang", "cn", "--sections"]) +def test_doc_chinese_language(capsys): + rc = cli_main.run(["doc", "config", "--lang", "cn", "--plain"]) out = capsys.readouterr().out assert rc == 0 - assert "8.5" in out - assert any("\u4e00" <= ch <= "\u9fff" for ch in out) - + assert any("一" <= ch <= "鿿" for ch in out) -def test_doc_sections_rejects_section_combination(capsys): - rc = cli_main.run(["doc", "config", "7", "--sections"]) - captured = capsys.readouterr() - assert rc == 1 - assert "Error:" in captured.err - - -def test_doc_unknown_section_lists_available_tokens(capsys): - rc = cli_main.run(["doc", "config", "99"]) - - captured = capsys.readouterr() - assert rc == 1 - assert "no section 99" in captured.err - for token in ("0", "7", "16"): - assert token in captured.err - assert "Configuration System Overview" in captured.err - - -def test_doc_without_section_shows_the_full_guide(capsys): - rc = cli_main.run(["doc", "tutorial", "1", "--plain"]) +def test_doc_default_text_output_keeps_unicode_layout_without_ansi(capsys): + rc = cli_main.run(["doc", "config"]) out = capsys.readouterr().out assert rc == 0 - assert out.startswith("## 1.") + assert "\x1b[" not in out + assert "─" in out -def test_doc_chinese_language(capsys): - rc = cli_main.run(["doc", "config", "--lang", "cn", "--plain"]) - - out = capsys.readouterr().out - assert rc == 0 - assert any("\u4e00" <= ch <= "\u9fff" for ch in out) +def test_doc_pages_the_full_guide_when_stdout_is_a_tty(monkeypatch, capsys): + import pydoc + paged = [] + monkeypatch.setattr(pydoc, "pager", paged.append) + monkeypatch.setattr(sys.stdout, "isatty", lambda: True) -def test_doc_default_text_output_keeps_unicode_layout_without_ansi(capsys): - rc = cli_main.run(["doc", "config", "1"]) + rc = cli_main.run(["doc", "config"]) - out = capsys.readouterr().out assert rc == 0 - assert "\x1b[" not in out - assert "\u2500" in out + assert capsys.readouterr().out == "" + assert len(paged) == 1 + assert "cts_ecc.json" in paged[0] @pytest.mark.parametrize( @@ -261,6 +131,7 @@ def test_doc_default_text_output_keeps_unicode_layout_without_ansi(capsys): ["doc", "CONFIG"], ["doc"], ["doc", "config", "--lang", "jp"], + ["doc", "config", "7"], ], ids=lambda argv: " ".join(argv), ) From e7f3a983e2ae143c76faf67afccf9d8cb094c3b2 Mon Sep 17 00:00:00 2001 From: Emin <me@emin.chat> Date: Mon, 7 Sep 2026 21:32:50 +0800 Subject: [PATCH 20/26] fix(tools-ecc): keep matplotlib off non-plot CLI paths such as ecc doctor Codex re-review found two remaining eager edges: runner.py imported ECCToolsPlot at module top (pulled in by the package __init__), and the tools.ecc package re-exported ECCToolsPlot eagerly, so 'ecc doctor' still imported matplotlib via chipcompiler.tools.ecc.utility. - Move the runner's ECCToolsPlot import into run_analysis, its only use. - Re-export ECCToolsPlot from the package lazily (same PEP 562 pattern as chipcompiler.utility). - Isolate MPLCONFIGDIR with tmp_path in the lazy-plot regression tests and extend them to cover the tools.ecc probe-import path. --- chipcompiler/tools/ecc/__init__.py | 22 ++++++++++++- chipcompiler/tools/ecc/runner.py | 3 +- test/tools/ecc/test_runner.py | 2 +- test/utility/test_plot_lazy.py | 51 +++++++++++++++++++++++------- 4 files changed, 64 insertions(+), 14 deletions(-) diff --git a/chipcompiler/tools/ecc/__init__.py b/chipcompiler/tools/ecc/__init__.py index 747ab34a..9b59a137 100644 --- a/chipcompiler/tools/ecc/__init__.py +++ b/chipcompiler/tools/ecc/__init__.py @@ -1,13 +1,33 @@ +from typing import TYPE_CHECKING + from .builder import build_step, build_step_config, build_step_space from .checklist import EccChecklist from .metrics import build_step_metrics from .module import ECCToolsModule -from .plot import ECCToolsPlot from .runner import create_db_engine, run_step from .service import get_step_info from .subflow import EccSubFlow, EccSubFlowEnum from .utility import is_eda_exist +# ECCToolsPlot pulls in matplotlib through the utility plot helpers, so it is +# re-exported lazily via PEP 562 instead of at package import time. +_PLOT_EXPORTS = frozenset({"ECCToolsPlot"}) + +if TYPE_CHECKING: + from .plot import ECCToolsPlot +else: + # Hidden from type checkers so unknown attributes still fail statically. + def __getattr__(name: str): + if name in _PLOT_EXPORTS: + from . import plot + + return getattr(plot, name) + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") + + def __dir__() -> list[str]: + return sorted(set(globals()) | _PLOT_EXPORTS) + + __all__ = [ "is_eda_exist", "build_default_flow", diff --git a/chipcompiler/tools/ecc/runner.py b/chipcompiler/tools/ecc/runner.py index a75e2179..08e61be5 100644 --- a/chipcompiler/tools/ecc/runner.py +++ b/chipcompiler/tools/ecc/runner.py @@ -18,7 +18,6 @@ save_rcx_spef_feature_facts, ) from chipcompiler.tools.ecc.module import ECCToolsModule -from chipcompiler.tools.ecc.plot import ECCToolsPlot from chipcompiler.tools.ecc.sta_artifacts import discard_sta_outputs from chipcompiler.tools.ecc.sta_qor import ( POST_SYNTHESIS_STA_CORNER, @@ -534,6 +533,8 @@ def run_analysis(workspace: Workspace, step: EccStep, subflow: EccSubFlow): build_step_metrics(workspace=workspace, step=step, subflow=subflow) # plot layout image + from chipcompiler.tools.ecc.plot import ECCToolsPlot + ploter = ECCToolsPlot(workspace=workspace, step=step) ploter.plot() diff --git a/test/tools/ecc/test_runner.py b/test/tools/ecc/test_runner.py index 20834682..ee9b2378 100644 --- a/test/tools/ecc/test_runner.py +++ b/test/tools/ecc/test_runner.py @@ -113,7 +113,7 @@ def test_run_analysis_switch(parameters, expected_calls, tmp_path, monkeypatch): plotter = Mock() checklist = Mock() monkeypatch.setattr(ecc_runner, "build_step_metrics", metrics) - monkeypatch.setattr(ecc_runner, "ECCToolsPlot", plotter) + monkeypatch.setattr("chipcompiler.tools.ecc.plot.ECCToolsPlot", plotter) monkeypatch.setattr(ecc_runner, "EccChecklist", checklist) ecc_runner.run_analysis(workspace=workspace, step=step, subflow=FakeSubFlow()) diff --git a/test/utility/test_plot_lazy.py b/test/utility/test_plot_lazy.py index 318a3d8a..71ed5fe3 100644 --- a/test/utility/test_plot_lazy.py +++ b/test/utility/test_plot_lazy.py @@ -5,60 +5,89 @@ matplotlib's import or its cold-cache fc-list font scan. """ +import os import subprocess import sys +from pathlib import Path -def _run_fresh(code: str) -> subprocess.CompletedProcess: +def _run_fresh(code: str, tmp_path: Path) -> subprocess.CompletedProcess: + env = os.environ | {"MPLCONFIGDIR": str(tmp_path / "mplconfig")} return subprocess.run( [sys.executable, "-c", code], capture_output=True, text=True, check=True, + env=env, ) -def test_utility_import_does_not_load_matplotlib(): - result = _run_fresh("import sys, chipcompiler.utility; print('matplotlib' in sys.modules)") +def test_utility_import_does_not_load_matplotlib(tmp_path): + result = _run_fresh( + "import sys, chipcompiler.utility; print('matplotlib' in sys.modules)", tmp_path + ) assert result.stdout.strip() == "False" -def test_cli_help_does_not_load_matplotlib(): +def test_cli_help_does_not_load_matplotlib(tmp_path): result = _run_fresh( "import sys; from chipcompiler.cli.main import run; " "rc = run(['--help']); " - "print(rc, 'matplotlib' in sys.modules)" + "print(rc, 'matplotlib' in sys.modules)", + tmp_path, ) assert result.stdout.strip().splitlines()[-1] == "0 False" -def test_plot_exports_resolve_lazily(): +def test_ecc_tools_probe_import_does_not_load_matplotlib(tmp_path): + # ecc doctor imports chipcompiler.tools.ecc.utility on a non-plot path. + result = _run_fresh( + "import sys; from chipcompiler.tools.ecc.utility import is_eda_exist; " + "print('matplotlib' in sys.modules)", + tmp_path, + ) + assert result.stdout.strip() == "False" + + +def test_plot_exports_resolve_lazily(tmp_path): result = _run_fresh( "import sys, chipcompiler.utility as u; " "names = ['plot_bar_chart', 'plot_csv_bar_chart', 'plot_csv_map', " "'plot_csv_table', 'plot_metrics']; " "funcs = [getattr(u, n) for n in names]; " - "print(all(callable(f) for f in funcs), 'matplotlib' in sys.modules)" + "print(all(callable(f) for f in funcs), 'matplotlib' in sys.modules)", + tmp_path, + ) + assert result.stdout.strip() == "True True" + + +def test_ecc_tools_plot_export_resolves_lazily(tmp_path): + result = _run_fresh( + "import sys; from chipcompiler.tools.ecc import ECCToolsPlot; " + "print(callable(ECCToolsPlot), 'matplotlib' in sys.modules)", + tmp_path, ) assert result.stdout.strip() == "True True" -def test_unknown_attribute_still_raises(): +def test_unknown_attribute_still_raises(tmp_path): result = _run_fresh( "import chipcompiler.utility as u\n" "try:\n" " u.no_such_name\n" "except AttributeError:\n" - " print('AttributeError')\n" + " print('AttributeError')\n", + tmp_path, ) assert result.stdout.strip() == "AttributeError" -def test_plot_exports_stay_discoverable(): +def test_plot_exports_stay_discoverable(tmp_path): result = _run_fresh( "import chipcompiler.utility as u; " "print(all(n in dir(u) and n in u.__all__ for n in " "['plot_bar_chart', 'plot_csv_bar_chart', 'plot_csv_map', " - "'plot_csv_table', 'plot_metrics']))" + "'plot_csv_table', 'plot_metrics']))", + tmp_path, ) assert result.stdout.strip() == "True" From 7395fcffac2f58b33271625c26c24555a31dcd2b Mon Sep 17 00:00:00 2001 From: Emin <me@emin.chat> Date: Mon, 7 Sep 2026 21:40:28 +0800 Subject: [PATCH 21/26] feat(cli): keep rich highlighting inside the ecc doc pager console.pager() flattened all styles, so the paged guide lost every heading and emphasis. Page with styles=color instead and default LESS=FRX when the user has no LESS of their own: pydoc spawns plain less, which escapes ANSI sequences unless -R is given. --- chipcompiler/cli/rendering/render.py | 15 ++++++++++-- docs/ecc-cli-ug.cn.md | 2 +- docs/ecc-cli-ug.en.md | 2 +- test/cli/test_doc.py | 34 ++++++++++++++++++++++++++++ 4 files changed, 49 insertions(+), 4 deletions(-) diff --git a/chipcompiler/cli/rendering/render.py b/chipcompiler/cli/rendering/render.py index 50fa0b1e..fbf0d480 100644 --- a/chipcompiler/cli/rendering/render.py +++ b/chipcompiler/cli/rendering/render.py @@ -1,4 +1,5 @@ import json +import os import sys from chipcompiler.cli.core.types import CommandResult, OutputMode @@ -60,8 +61,18 @@ def render_markdown(text: str, file=None, *, color: bool, pager: bool = False) - # Page only on a real terminal: pydoc picks its pager at import time, so # its own isatty check cannot be trusted once the process has been piped. if pager and file is None and sys.stdout.isatty(): - with console.pager(): - console.print(Markdown(text)) + # pydoc invokes plain `less`, which escapes ANSI; with no user LESS, + # default to git's FRX so styled output renders and short docs don't + # open the pager UI at all. + saved_less = os.environ.get("LESS") + if saved_less is None: + os.environ["LESS"] = "FRX" + try: + with console.pager(styles=color): + console.print(Markdown(text)) + finally: + if saved_less is None: + del os.environ["LESS"] else: console.print(Markdown(text)) diff --git a/docs/ecc-cli-ug.cn.md b/docs/ecc-cli-ug.cn.md index cbc464f1..a192addf 100644 --- a/docs/ecc-cli-ug.cn.md +++ b/docs/ecc-cli-ug.cn.md @@ -125,7 +125,7 @@ ecc doc config --plain # 原始 markdown,逐字节输出 ``` - 主题:`config`、`ug`、`tutorial`、`dev`;`--lang` 选择 `en`(默认)或 `cn`。 -- 终端下渲染输出会进入分页器(`$PAGER`,回退到 `less`/`more`)翻阅;管道场景直接全量输出。 +- 终端下渲染输出带高亮并进入分页器翻阅(`$PAGER`,回退到 `less`/`more`;未设置 `LESS` 时默认 `LESS=FRX`,保证 `less` 下颜色生效);管道场景全量直出、不带颜色。 - 非法的主题/语言取值由参数校验拒绝(退出码 2)。 - 管道输出保留 unicode 渲染版式;`--plain` 原样输出原始 markdown,适合脚本处理。 diff --git a/docs/ecc-cli-ug.en.md b/docs/ecc-cli-ug.en.md index 7959e303..5c65fb8d 100644 --- a/docs/ecc-cli-ug.en.md +++ b/docs/ecc-cli-ug.en.md @@ -125,7 +125,7 @@ ecc doc config --plain # raw markdown, byte-for-byte ``` - Topics: `config`, `ug`, `tutorial`, `dev`; `--lang` selects `en` (default) or `cn`. -- On a terminal the rendered guide opens in a pager (`$PAGER`, falling back to `less`/`more`); when piped it prints in full. +- On a terminal the rendered guide opens in a pager with highlighting (`$PAGER`, falling back to `less`/`more`; `LESS=FRX` is defaulted when unset so colors survive `less`). When piped it prints in full without colors. - Invalid topic/language values are rejected by argument validation (exit 2). - Default output keeps the rendered unicode layout even when piped; `--plain` prints the raw markdown unchanged (script-friendly). diff --git a/test/cli/test_doc.py b/test/cli/test_doc.py index d599f890..8450e4ed 100644 --- a/test/cli/test_doc.py +++ b/test/cli/test_doc.py @@ -115,6 +115,7 @@ def test_doc_pages_the_full_guide_when_stdout_is_a_tty(monkeypatch, capsys): paged = [] monkeypatch.setattr(pydoc, "pager", paged.append) monkeypatch.setattr(sys.stdout, "isatty", lambda: True) + monkeypatch.setattr("chipcompiler.cli.commands.doc.supports_color", lambda: False) rc = cli_main.run(["doc", "config"]) @@ -122,6 +123,39 @@ def test_doc_pages_the_full_guide_when_stdout_is_a_tty(monkeypatch, capsys): assert capsys.readouterr().out == "" assert len(paged) == 1 assert "cts_ecc.json" in paged[0] + assert "\x1b[" not in paged[0] + + +def test_doc_pager_keeps_styles_when_color_is_supported(monkeypatch, capsys): + import pydoc + + paged = [] + monkeypatch.setattr(pydoc, "pager", paged.append) + monkeypatch.setattr(sys.stdout, "isatty", lambda: True) + monkeypatch.setattr("chipcompiler.cli.commands.doc.supports_color", lambda: True) + + rc = cli_main.run(["doc", "config"]) + + assert rc == 0 + assert capsys.readouterr().out == "" + assert len(paged) == 1 + assert "\x1b[" in paged[0] + + +def test_doc_pager_defaults_less_and_restores_the_environment(monkeypatch, capsys): + import os + import pydoc + + seen = [] + monkeypatch.setattr(pydoc, "pager", lambda text: seen.append(os.environ.get("LESS"))) + monkeypatch.setattr(sys.stdout, "isatty", lambda: True) + monkeypatch.delenv("LESS", raising=False) + + rc = cli_main.run(["doc", "config"]) + + assert rc == 0 + assert seen == ["FRX"] + assert "LESS" not in os.environ @pytest.mark.parametrize( From b4d9e5e8c36150b0f4667ab5dec9ccabd5954527 Mon Sep 17 00:00:00 2001 From: Emin <me@emin.chat> Date: Mon, 7 Sep 2026 22:20:42 +0800 Subject: [PATCH 22/26] feat(cli): ship the ecc doc guides inside the wheel Move the eight CLI guides from the repo-root docs/ into chipcompiler/docs/ so uv_build includes them in the wheel; only files under the module directory are packaged. guides_root() now resolves them through importlib.resources, which covers dev checkouts, editable installs, and wheel installs with one path; the _MEIPASS branch still serves the PyInstaller bundle. Relative links in the guides, docs/index.md, and the PyInstaller spec datas are updated for the new location. --- chipcompiler/cli/core/docs.py | 8 ++++---- {docs => chipcompiler/docs}/ecc-cli-config.cn.md | 8 ++++---- {docs => chipcompiler/docs}/ecc-cli-config.en.md | 8 ++++---- {docs => chipcompiler/docs}/ecc-cli-dev.cn.md | 8 ++++---- {docs => chipcompiler/docs}/ecc-cli-dev.en.md | 6 +++--- .../docs}/ecc-cli-tutorial.cn.md | 2 +- .../docs}/ecc-cli-tutorial.en.md | 2 +- {docs => chipcompiler/docs}/ecc-cli-ug.cn.md | 2 +- {docs => chipcompiler/docs}/ecc-cli-ug.en.md | 2 +- docs/index.md | 16 ++++++++-------- ecc.spec | 16 ++++++++-------- test/cli/test_doc.py | 2 +- test/packaging/test_cli_entrypoint.py | 2 +- 13 files changed, 41 insertions(+), 41 deletions(-) rename {docs => chipcompiler/docs}/ecc-cli-config.cn.md (98%) rename {docs => chipcompiler/docs}/ecc-cli-config.en.md (98%) rename {docs => chipcompiler/docs}/ecc-cli-dev.cn.md (96%) rename {docs => chipcompiler/docs}/ecc-cli-dev.en.md (97%) rename {docs => chipcompiler/docs}/ecc-cli-tutorial.cn.md (99%) rename {docs => chipcompiler/docs}/ecc-cli-tutorial.en.md (99%) rename {docs => chipcompiler/docs}/ecc-cli-ug.cn.md (99%) rename {docs => chipcompiler/docs}/ecc-cli-ug.en.md (99%) diff --git a/chipcompiler/cli/core/docs.py b/chipcompiler/cli/core/docs.py index d6e9202b..f669731a 100644 --- a/chipcompiler/cli/core/docs.py +++ b/chipcompiler/cli/core/docs.py @@ -1,6 +1,8 @@ """Locate and load the bundled CLI guide documents.""" import sys +from importlib import resources +from importlib.resources.abc import Traversable from pathlib import Path GUIDE_STEMS = { @@ -15,13 +17,11 @@ class GuideNotFoundError(FileNotFoundError): pass -def guides_root() -> Path: +def guides_root() -> Traversable: bundle_root = getattr(sys, "_MEIPASS", None) if bundle_root: return Path(bundle_root) / "docs" - import chipcompiler - - return Path(chipcompiler.__file__).resolve().parent.parent / "docs" + return resources.files("chipcompiler") / "docs" def load_guide(topic: str, lang: str) -> bytes: diff --git a/docs/ecc-cli-config.cn.md b/chipcompiler/docs/ecc-cli-config.cn.md similarity index 98% rename from docs/ecc-cli-config.cn.md rename to chipcompiler/docs/ecc-cli-config.cn.md index ca31769f..6a8dd536 100644 --- a/docs/ecc-cli-config.cn.md +++ b/chipcompiler/docs/ecc-cli-config.cn.md @@ -1,6 +1,6 @@ # ECC Flow 工具配置参考(按步骤) -本文整理 ECC RTL-to-Harden 流程中**每一步实际使用的工具配置文件、全部参数及其含义**。配置取值与生成逻辑均核对自 v0.1.0-alpha.11 源码(rebase main 之后;模板位于 [chipcompiler/tools/*/configs/](../chipcompiler/tools/ecc/configs/))与一次真实的 gcd@ics55 harden 运行。 +本文整理 ECC RTL-to-Harden 流程中**每一步实际使用的工具配置文件、全部参数及其含义**。配置取值与生成逻辑均核对自 v0.1.0-alpha.11 源码(rebase main 之后;模板位于 [chipcompiler/tools/*/configs/](../../chipcompiler/tools/ecc/configs/))与一次真实的 gcd@ics55 harden 运行。 - 想了解命令用法 → [ECC CLI 用户指南](ecc-cli-ug.cn.md);从零上手 → [入门教程](ecc-cli-tutorial.cn.md) - 配置查看命令:`ecc config <step>`(列出该步骤实际生效的配置文件);参数查看与修改命令:`ecc param`(见 §1.4) @@ -61,7 +61,7 @@ graph LR ### 0.3 每个步骤用到哪些配置 -`ecc config <step>` 的真实输出归纳(映射源码 `_STEP_CONFIG_KEYS`,位于 [chipcompiler/data/workspace/__init__.py](../chipcompiler/data/workspace/__init__.py)): +`ecc config <step>` 的真实输出归纳(映射源码 `_STEP_CONFIG_KEYS`,位于 [chipcompiler/data/workspace/__init__.py](../../chipcompiler/data/workspace/__init__.py)): | 步骤 | db_ecc | 专属配置 | 说明 | |---|---|---|---| @@ -85,7 +85,7 @@ graph LR ### 1.1 旧语义参数(13 个) -来源:[chipcompiler/cli/project/params.py](../chipcompiler/cli/project/params.py) 的 `_LEGACY_PARAM_REGISTRY`(`PARAM_REGISTRY` 的兼容段;直配参数见 §1.2 的 `config_params/` schema)。这些参数保持兼容;优先级:`--set` > `ecc.toml [params]` > 默认值。「写入位置」列为该参数最终落到的工具配置字段。 +来源:[chipcompiler/cli/project/params.py](../../chipcompiler/cli/project/params.py) 的 `_LEGACY_PARAM_REGISTRY`(`PARAM_REGISTRY` 的兼容段;直配参数见 §1.2 的 `config_params/` schema)。这些参数保持兼容;优先级:`--set` > `ecc.toml [params]` > 默认值。「写入位置」列为该参数最终落到的工具配置字段。 | 参数 | 类型 / 范围 | 默认 | 写入位置(config 字段) | 含义 | |---|---|---|---|---| @@ -152,7 +152,7 @@ tech = "prtech/techLEF/N551P6M_ecos.lef" ### 1.4 参数配置 CLI 命令(`ecc param`) -参数的查看与修改统一走 `ecc param` 子命令(子命令定义:[chipcompiler/cli/commands/param.py](../chipcompiler/cli/commands/param.py);项目作用域实现:[chipcompiler/cli/command_handlers/param.py](../chipcompiler/cli/command_handlers/param.py),workspace 作用域实现:[chipcompiler/cli/command_handlers/workspace_params.py](../chipcompiler/cli/command_handlers/workspace_params.py)): +参数的查看与修改统一走 `ecc param` 子命令(子命令定义:[chipcompiler/cli/commands/param.py](../../chipcompiler/cli/commands/param.py);项目作用域实现:[chipcompiler/cli/command_handlers/param.py](../../chipcompiler/cli/command_handlers/param.py),workspace 作用域实现:[chipcompiler/cli/command_handlers/workspace_params.py](../../chipcompiler/cli/command_handlers/workspace_params.py)): | 命令 | 作用 | |---|---| diff --git a/docs/ecc-cli-config.en.md b/chipcompiler/docs/ecc-cli-config.en.md similarity index 98% rename from docs/ecc-cli-config.en.md rename to chipcompiler/docs/ecc-cli-config.en.md index 4e5623bb..79eb1fdc 100644 --- a/docs/ecc-cli-config.en.md +++ b/chipcompiler/docs/ecc-cli-config.en.md @@ -1,6 +1,6 @@ # ECC Flow Tool Configuration Reference (by step) -This document consolidates **the tool configuration files actually used by each step of the ECC RTL-to-Harden flow, all of their parameters, and what each parameter means**. Configuration values and generation logic were verified against the v0.1.0-alpha.11 source (after the rebase onto main; templates live in [chipcompiler/tools/*/configs/](../chipcompiler/tools/ecc/configs/)) and a real gcd@ics55 harden run. +This document consolidates **the tool configuration files actually used by each step of the ECC RTL-to-Harden flow, all of their parameters, and what each parameter means**. Configuration values and generation logic were verified against the v0.1.0-alpha.11 source (after the rebase onto main; templates live in [chipcompiler/tools/*/configs/](../../chipcompiler/tools/ecc/configs/)) and a real gcd@ics55 harden run. - For command usage, see the [ECC CLI User Guide](ecc-cli-ug.en.md); to get started from scratch, see the [Tutorial](ecc-cli-tutorial.en.md) - Config inspection command: `ecc config <step>` (lists the configuration files actually in effect for that step); parameter inspection/modification command: `ecc param` (see §1.4) @@ -61,7 +61,7 @@ graph LR ### 0.3 Which configurations each step uses -Distilled from real `ecc config <step>` output (maps to the source `_STEP_CONFIG_KEYS` in [chipcompiler/data/workspace/__init__.py](../chipcompiler/data/workspace/__init__.py)): +Distilled from real `ecc config <step>` output (maps to the source `_STEP_CONFIG_KEYS` in [chipcompiler/data/workspace/__init__.py](../../chipcompiler/data/workspace/__init__.py)): | Step | db_ecc | Step-specific config | Notes | |---|---|---|---| @@ -85,7 +85,7 @@ Distilled from real `ecc config <step>` output (maps to the source `_STEP_CONFIG ### 1.1 Legacy-semantic parameters (13) -Source: `_LEGACY_PARAM_REGISTRY` in [chipcompiler/cli/project/params.py](../chipcompiler/cli/project/params.py) (the compatibility section of `PARAM_REGISTRY`; the direct-config parameters are the `config_params/` schemas in §1.2). These parameters are kept for compatibility; precedence: `--set` > `ecc.toml [params]` > defaults. The "Written to" column shows the tool configuration field each parameter ultimately lands in. +Source: `_LEGACY_PARAM_REGISTRY` in [chipcompiler/cli/project/params.py](../../chipcompiler/cli/project/params.py) (the compatibility section of `PARAM_REGISTRY`; the direct-config parameters are the `config_params/` schemas in §1.2). These parameters are kept for compatibility; precedence: `--set` > `ecc.toml [params]` > defaults. The "Written to" column shows the tool configuration field each parameter ultimately lands in. | Parameter | Type / range | Default | Written to (config field) | Meaning | |---|---|---|---|---| @@ -150,7 +150,7 @@ Workspace-local overrides written by `ecc param set KEY VALUE --workspace NAME` ### 1.4 Parameter-configuration CLI commands (`ecc param`) -Parameter inspection and modification go through the `ecc param` subcommands (subcommand definitions: [chipcompiler/cli/commands/param.py](../chipcompiler/cli/commands/param.py); project-scope implementation: [chipcompiler/cli/command_handlers/param.py](../chipcompiler/cli/command_handlers/param.py), workspace-scope implementation: [chipcompiler/cli/command_handlers/workspace_params.py](../chipcompiler/cli/command_handlers/workspace_params.py)): +Parameter inspection and modification go through the `ecc param` subcommands (subcommand definitions: [chipcompiler/cli/commands/param.py](../../chipcompiler/cli/commands/param.py); project-scope implementation: [chipcompiler/cli/command_handlers/param.py](../../chipcompiler/cli/command_handlers/param.py), workspace-scope implementation: [chipcompiler/cli/command_handlers/workspace_params.py](../../chipcompiler/cli/command_handlers/workspace_params.py)): | Command | What it does | |---|---| diff --git a/docs/ecc-cli-dev.cn.md b/chipcompiler/docs/ecc-cli-dev.cn.md similarity index 96% rename from docs/ecc-cli-dev.cn.md rename to chipcompiler/docs/ecc-cli-dev.cn.md index 068e54e5..1a72005e 100644 --- a/docs/ecc-cli-dev.cn.md +++ b/chipcompiler/docs/ecc-cli-dev.cn.md @@ -2,7 +2,7 @@ 本文面向需要在 `ecc` CLI 中新增/修改命令的开发者,基于 `ecc/` 子模块当前源码(`chipcompiler` 包,v0.1.0-alpha.11)整理。代码路径均相对 `ecc/` 子模块根目录。 -相关文档:[architecture.md](architecture.md)(架构)、[development.md](development.md)(开发工作流)、[workspace-cli.md](workspace-cli.md)(RPC sidecar 协议)、[../CLAUDE.md](../CLAUDE.md)(仓库约定)。 +相关文档:[architecture.md](../../docs/architecture.md)(架构)、[development.md](../../docs/development.md)(开发工作流)、[workspace-cli.md](../../docs/workspace-cli.md)(RPC sidecar 协议)、[../../CLAUDE.md](../../CLAUDE.md)(仓库约定)。 ## 1. 入口与整体结构 @@ -227,7 +227,7 @@ config_param( ### 5.6 扩展 RPC(`ecc rpc serve`) -`rpc serve --stdio` 启动 JSON-RPC 2.0 sidecar(`chipcompiler/runtime/stdio_server.py`)。方法在 `chipcompiler/runtime/methods.py::RUNTIME_METHODS` 声明(`method_name` + pydantic `request_model` + `handler_name`),handler 实现在 `chipcompiler/runtime/workspace_api.py`,由 `runtime/server.py` 统一挂载;协议细节见 [workspace-cli.md](workspace-cli.md)。新增方法 = 加一个 `RuntimeMethodSpec` + 对应 API 方法 + 请求模型,无需改 CLI 层。 +`rpc serve --stdio` 启动 JSON-RPC 2.0 sidecar(`chipcompiler/runtime/stdio_server.py`)。方法在 `chipcompiler/runtime/methods.py::RUNTIME_METHODS` 声明(`method_name` + pydantic `request_model` + `handler_name`),handler 实现在 `chipcompiler/runtime/workspace_api.py`,由 `runtime/server.py` 统一挂载;协议细节见 [workspace-cli.md](../../docs/workspace-cli.md)。新增方法 = 加一个 `RuntimeMethodSpec` + 对应 API 方法 + 请求模型,无需改 CLI 层。 ### 5.7 扩展项目声明(`ecc project *` / `ecc workspace refresh`) @@ -250,11 +250,11 @@ rm -rf ~/.local/ecc && mkdir -p ~/.local/ecc && cp -a dist/ecc/. ~/.local/ecc/ ecc --help # 验证 doctor / signoff / report 已列出 ``` -回退官方发行版:重新运行 [README](../README.cn.md#安装) 的安装脚本(`curl -fsSL http://release.openecos.com/installers/ecc/latest/ecc-installer.sh | sh`)。 +回退官方发行版:重新运行 [README](../../README.cn.md#安装) 的安装脚本(`curl -fsSL http://release.openecos.com/installers/ecc/latest/ecc-installer.sh | sh`)。 ## 7. 约束与注意事项(来自仓库约定) -- **模块体积**:文件超过约 800 LoC 时新功能放新模块,不要继续堆([../CLAUDE.md](../CLAUDE.md) 第 6 节)。 +- **模块体积**:文件超过约 800 LoC 时新功能放新模块,不要继续堆([../../CLAUDE.md](../../CLAUDE.md) 第 6 节)。 - **Python 3+**:不用 `__future__`;最低版本看 `pyproject.toml` 的 `requires-python`。 - **测试放置**按所有权边界;优先整对象比较;不为静态定义的值写测试;不为已删除的逻辑保留负向测试。 - **代码评审**必须执行 [review-guidelines.md](review-guidelines.md) 的附加标准。 diff --git a/docs/ecc-cli-dev.en.md b/chipcompiler/docs/ecc-cli-dev.en.md similarity index 97% rename from docs/ecc-cli-dev.en.md rename to chipcompiler/docs/ecc-cli-dev.en.md index 36daa779..69199631 100644 --- a/docs/ecc-cli-dev.en.md +++ b/chipcompiler/docs/ecc-cli-dev.en.md @@ -2,7 +2,7 @@ This guide is for developers who need to add or modify commands in the `ecc` CLI. It is based on the current source tree (the `chipcompiler` package, v0.1.0-alpha.11). All code paths are relative to the `ecc` repository root. -Related documents: [architecture.md](architecture.md) (architecture), [development.md](development.md) (development workflow), [workspace-cli.md](workspace-cli.md) (RPC sidecar protocol), [../CLAUDE.md](../CLAUDE.md) (repository conventions). +Related documents: [architecture.md](../../docs/architecture.md) (architecture), [development.md](../../docs/development.md) (development workflow), [workspace-cli.md](../../docs/workspace-cli.md) (RPC sidecar protocol), [../../CLAUDE.md](../../CLAUDE.md) (repository conventions). ## 1. Entry point and overall structure @@ -227,7 +227,7 @@ Project preset sequences are defined in `chipcompiler/rtl2gds/builder.py` (`buil ### 5.6 Extending the RPC (`ecc rpc serve`) -`rpc serve --stdio` starts the JSON-RPC 2.0 sidecar (`chipcompiler/runtime/stdio_server.py`). Methods are declared in `chipcompiler/runtime/methods.py::RUNTIME_METHODS` (`method_name` + a pydantic `request_model` + `handler_name`), handler implementations live in `chipcompiler/runtime/workspace_api.py`, and `runtime/server.py` mounts them uniformly; protocol details in [workspace-cli.md](workspace-cli.md). Adding a method = one `RuntimeMethodSpec` + the matching API method + a request model; no CLI-layer changes needed. +`rpc serve --stdio` starts the JSON-RPC 2.0 sidecar (`chipcompiler/runtime/stdio_server.py`). Methods are declared in `chipcompiler/runtime/methods.py::RUNTIME_METHODS` (`method_name` + a pydantic `request_model` + `handler_name`), handler implementations live in `chipcompiler/runtime/workspace_api.py`, and `runtime/server.py` mounts them uniformly; protocol details in [workspace-cli.md](../../docs/workspace-cli.md). Adding a method = one `RuntimeMethodSpec` + the matching API method + a request model; no CLI-layer changes needed. ### 5.7 Extending project declarations (`ecc project *` / `ecc workspace refresh`) @@ -250,7 +250,7 @@ rm -rf ~/.local/ecc && mkdir -p ~/.local/ecc && cp -a dist/ecc/. ~/.local/ecc/ ecc --help # verify doctor / signoff / report are listed ``` -To roll back to the official release, re-run the [README](../README.md#installation) installer (`curl -fsSL http://release.openecos.com/installers/ecc/latest/ecc-installer.sh | sh`). +To roll back to the official release, re-run the [README](../../README.md#installation) installer (`curl -fsSL http://release.openecos.com/installers/ecc/latest/ecc-installer.sh | sh`). ## 7. Constraints and caveats (from the repository conventions) diff --git a/docs/ecc-cli-tutorial.cn.md b/chipcompiler/docs/ecc-cli-tutorial.cn.md similarity index 99% rename from docs/ecc-cli-tutorial.cn.md rename to chipcompiler/docs/ecc-cli-tutorial.cn.md index 56f52f24..974e955e 100644 --- a/docs/ecc-cli-tutorial.cn.md +++ b/chipcompiler/docs/ecc-cli-tutorial.cn.md @@ -59,7 +59,7 @@ export PATH="$HOME/.local/bin:$PATH" ### 2.2 从源码运行(可选) -按 [README](../README.cn.md#源码构建) 带 `--recursive` 克隆仓库(`chipcompiler/thirdparty/` 会拉取 `ecc-tools` 和 `ecc-dreamplace`),再参照 [开发指南](development.md) 配置 `uv` 工作区: +按 [README](../../README.cn.md#源码构建) 带 `--recursive` 克隆仓库(`chipcompiler/thirdparty/` 会拉取 `ecc-tools` 和 `ecc-dreamplace`),再参照 [开发指南](../../docs/development.md) 配置 `uv` 工作区: ```bash git clone --recursive https://github.com/openecos-projects/ecc.git diff --git a/docs/ecc-cli-tutorial.en.md b/chipcompiler/docs/ecc-cli-tutorial.en.md similarity index 99% rename from docs/ecc-cli-tutorial.en.md rename to chipcompiler/docs/ecc-cli-tutorial.en.md index 4b170c0d..7d7aeb0a 100644 --- a/docs/ecc-cli-tutorial.en.md +++ b/chipcompiler/docs/ecc-cli-tutorial.en.md @@ -59,7 +59,7 @@ The `--with-toolchain` wrapper exports `CHIPCOMPILER_OSS_CAD_DIR` and `CHIPCOMPI ### 2.2 Running from source (optional) -Clone the repository with `--recursive` as the [README](../README.md#build-from-source) describes (`chipcompiler/thirdparty/` pulls in `ecc-tools` and `ecc-dreamplace`), then set up the `uv` workspace per the [development guide](development.md): +Clone the repository with `--recursive` as the [README](../../README.md#build-from-source) describes (`chipcompiler/thirdparty/` pulls in `ecc-tools` and `ecc-dreamplace`), then set up the `uv` workspace per the [development guide](../../docs/development.md): ```bash git clone --recursive https://github.com/openecos-projects/ecc.git diff --git a/docs/ecc-cli-ug.cn.md b/chipcompiler/docs/ecc-cli-ug.cn.md similarity index 99% rename from docs/ecc-cli-ug.cn.md rename to chipcompiler/docs/ecc-cli-ug.cn.md index a192addf..893278c9 100644 --- a/docs/ecc-cli-ug.cn.md +++ b/chipcompiler/docs/ecc-cli-ug.cn.md @@ -2,7 +2,7 @@ `ecc` 是 ECOS Chip Compiler 的项目制命令行入口,覆盖 RTL-to-GDS 流水的建项、校验、运行、状态/日志/配置查询、参数管理、签核与报告。本文基于 `ecc/` 子模块当前源码(v0.1.0-alpha.11)整理,所有示例输出均为真实执行结果(示例中的 run 状态为手工构造的演示数据)。 -- 源码位置:[chipcompiler/cli/](../chipcompiler/cli/) +- 源码位置:[chipcompiler/cli/](../../chipcompiler/cli/) - 命令扩展开发方式见同目录 [ecc-cli-dev.cn.md](ecc-cli-dev.cn.md) - RPC sidecar 协议详见 [workspace-cli.md](workspace-cli.md) diff --git a/docs/ecc-cli-ug.en.md b/chipcompiler/docs/ecc-cli-ug.en.md similarity index 99% rename from docs/ecc-cli-ug.en.md rename to chipcompiler/docs/ecc-cli-ug.en.md index 5c65fb8d..1ac7a897 100644 --- a/docs/ecc-cli-ug.en.md +++ b/chipcompiler/docs/ecc-cli-ug.en.md @@ -2,7 +2,7 @@ `ecc` is the project-oriented command-line entry point of ECOS Chip Compiler, covering the full RTL-to-GDS flow: project creation, validation, execution, status/log/config inspection, parameter management, signoff, and reporting. This guide is based on the current source tree (v0.1.0-alpha.11); all example outputs are real execution results (run states in the examples are hand-crafted demo data). -- Source code: [chipcompiler/cli/](../chipcompiler/cli/) +- Source code: [chipcompiler/cli/](../../chipcompiler/cli/) - For how to extend the CLI with new commands, see [ecc-cli-dev.en.md](ecc-cli-dev.en.md) - RPC sidecar protocol: [workspace-cli.md](workspace-cli.md) diff --git a/docs/index.md b/docs/index.md index c25a51ee..a53387d2 100644 --- a/docs/index.md +++ b/docs/index.md @@ -6,15 +6,15 @@ Welcome to the ChipCompiler documentation center. The `ecc` command-line tool ships bilingual guides (`.en.md` / `.cn.md`): -- **[CLI Tutorial](ecc-cli-tutorial.en.md)** / **[中文教程](ecc-cli-tutorial.cn.md)** - From zero to RTL → Harden with a signoff package +- **[CLI Tutorial](../chipcompiler/docs/ecc-cli-tutorial.en.md)** / **[中文教程](../chipcompiler/docs/ecc-cli-tutorial.cn.md)** - From zero to RTL → Harden with a signoff package - Installing the ecc CLI, PDK, and Yosys - First project, the 15-step `rtl2gds` flow, signoff package, and reports - Tuning parameters, workspaces, and rerun scenarios -- **[CLI User Guide](ecc-cli-ug.en.md)** / **[中文用户指南](ecc-cli-ug.cn.md)** - All currently supported commands +- **[CLI User Guide](../chipcompiler/docs/ecc-cli-ug.en.md)** / **[中文用户指南](../chipcompiler/docs/ecc-cli-ug.cn.md)** - All currently supported commands - Every command and option: `init`/`check`/`run`/`status`/`log`/`config`/`doctor`/`param`/`pdk`/`project`/`workspace`/`signoff`/`report`/`rpc`/`layout-image` - Run selectors (`--resume`/`--from`/`--to`/`--only`), error-code reference, end-to-end workflows -- **[CLI Config Reference](ecc-cli-config.en.md)** / **[中文配置参考](ecc-cli-config.cn.md)** - `ecc.toml`, workspace files, and the parameter system -- **[CLI Dev Guide](ecc-cli-dev.en.md)** / **[中文开发指南](ecc-cli-dev.cn.md)** - Adding or modifying CLI commands +- **[CLI Config Reference](../chipcompiler/docs/ecc-cli-config.en.md)** / **[中文配置参考](../chipcompiler/docs/ecc-cli-config.cn.md)** - `ecc.toml`, workspace files, and the parameter system +- **[CLI Dev Guide](../chipcompiler/docs/ecc-cli-dev.en.md)** / **[中文开发指南](../chipcompiler/docs/ecc-cli-dev.cn.md)** - Adding or modifying CLI commands - **[Workspace CLI Guide](workspace-cli.md)** - Private JSON-RPC runtime sidecar protocol (`ecc rpc serve`) ## Core Documentation @@ -58,10 +58,10 @@ ChipCompiler supports various EDA file formats. Technical specifications for par ### I want to... - **Get started with ChipCompiler** → See main [README](../README.md) -- **Run my first RTL-to-GDS flow** → [CLI Tutorial](ecc-cli-tutorial.en.md) / [中文教程](ecc-cli-tutorial.cn.md) -- **Look up an `ecc` command or option** → [CLI User Guide](ecc-cli-ug.en.md) / [中文用户指南](ecc-cli-ug.cn.md) -- **Understand `ecc.toml` / workspace files / parameters** → [CLI Config Reference](ecc-cli-config.en.md) / [中文配置参考](ecc-cli-config.cn.md) -- **Extend the CLI with new commands** → [CLI Dev Guide](ecc-cli-dev.en.md) +- **Run my first RTL-to-GDS flow** → [CLI Tutorial](../chipcompiler/docs/ecc-cli-tutorial.en.md) / [中文教程](../chipcompiler/docs/ecc-cli-tutorial.cn.md) +- **Look up an `ecc` command or option** → [CLI User Guide](../chipcompiler/docs/ecc-cli-ug.en.md) / [中文用户指南](../chipcompiler/docs/ecc-cli-ug.cn.md) +- **Understand `ecc.toml` / workspace files / parameters** → [CLI Config Reference](../chipcompiler/docs/ecc-cli-config.en.md) / [中文配置参考](../chipcompiler/docs/ecc-cli-config.cn.md) +- **Extend the CLI with new commands** → [CLI Dev Guide](../chipcompiler/docs/ecc-cli-dev.en.md) - **Use legacy workspace commands** → [Workspace CLI Guide](workspace-cli.md) - **Understand the architecture** → [Architecture](architecture.md) - **Set up development environment** → [Development Guide](development.md) diff --git a/ecc.spec b/ecc.spec index c986db8f..528444ce 100644 --- a/ecc.spec +++ b/ecc.spec @@ -53,14 +53,14 @@ DREAMPLACE_THIRDPARTY_FILES = ( ) DOC_GUIDES = ( - "docs/ecc-cli-config.en.md", - "docs/ecc-cli-config.cn.md", - "docs/ecc-cli-ug.en.md", - "docs/ecc-cli-ug.cn.md", - "docs/ecc-cli-tutorial.en.md", - "docs/ecc-cli-tutorial.cn.md", - "docs/ecc-cli-dev.en.md", - "docs/ecc-cli-dev.cn.md", + "chipcompiler/docs/ecc-cli-config.en.md", + "chipcompiler/docs/ecc-cli-config.cn.md", + "chipcompiler/docs/ecc-cli-ug.en.md", + "chipcompiler/docs/ecc-cli-ug.cn.md", + "chipcompiler/docs/ecc-cli-tutorial.en.md", + "chipcompiler/docs/ecc-cli-tutorial.cn.md", + "chipcompiler/docs/ecc-cli-dev.en.md", + "chipcompiler/docs/ecc-cli-dev.cn.md", ) LINUX_RUNTIME_LIBS = ( diff --git a/test/cli/test_doc.py b/test/cli/test_doc.py index 8450e4ed..02906128 100644 --- a/test/cli/test_doc.py +++ b/test/cli/test_doc.py @@ -9,7 +9,7 @@ def test_guides_root_points_at_repository_docs_in_dev_mode(): - repo_docs = Path(__file__).parents[2] / "docs" + repo_docs = Path(__file__).parents[2] / "chipcompiler" / "docs" assert guides_root() == repo_docs assert (guides_root() / "ecc-cli-config.en.md").is_file() diff --git a/test/packaging/test_cli_entrypoint.py b/test/packaging/test_cli_entrypoint.py index c4aa3872..36251e45 100644 --- a/test/packaging/test_cli_entrypoint.py +++ b/test/packaging/test_cli_entrypoint.py @@ -45,7 +45,7 @@ def test_pyinstaller_spec_collects_doc_guides(self): assert "datas.extend(collect_doc_guides())" in source for stem in ("config", "ug", "tutorial", "dev"): for lang in ("en", "cn"): - assert f"docs/ecc-cli-{stem}.{lang}.md" in source + assert f"chipcompiler/docs/ecc-cli-{stem}.{lang}.md" in source def test_pyinstaller_spec_collects_rich_unicode_data_modules(self): project_root = os.path.dirname(os.path.dirname(os.path.dirname(__file__))) From b03d7225ab9b6873b826bae9c026383e79cf3c4b Mon Sep 17 00:00:00 2001 From: Emin <me@emin.chat> Date: Mon, 7 Sep 2026 22:33:10 +0800 Subject: [PATCH 23/26] fix(cli): repair moved guide links and pin doc packaging contracts Codex review of dc83f231 found relative links that never pointed through ../ and were missed by the move (docs/examples/gcd, docs/specification, workspace-cli.md, review-guidelines.md), now resolved as ../../docs/... from chipcompiler/docs/. Add artifact-level guards: a packaging test builds the wheel and asserts all eight guides are inside, and the PyInstaller bundle smoke test in CI now runs ecc doc for an English and a Chinese guide. --- .../build-pyinstaller-bundle/action.yml | 2 ++ chipcompiler/docs/ecc-cli-dev.cn.md | 2 +- chipcompiler/docs/ecc-cli-dev.en.md | 2 +- chipcompiler/docs/ecc-cli-tutorial.cn.md | 8 +++---- chipcompiler/docs/ecc-cli-tutorial.en.md | 8 +++---- chipcompiler/docs/ecc-cli-ug.cn.md | 4 ++-- chipcompiler/docs/ecc-cli-ug.en.md | 4 ++-- test/packaging/test_wheel_contents.py | 24 +++++++++++++++++++ 8 files changed, 40 insertions(+), 14 deletions(-) create mode 100644 test/packaging/test_wheel_contents.py diff --git a/.github/actions/build-pyinstaller-bundle/action.yml b/.github/actions/build-pyinstaller-bundle/action.yml index e2b873ca..5ba0309e 100644 --- a/.github/actions/build-pyinstaller-bundle/action.yml +++ b/.github/actions/build-pyinstaller-bundle/action.yml @@ -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" diff --git a/chipcompiler/docs/ecc-cli-dev.cn.md b/chipcompiler/docs/ecc-cli-dev.cn.md index 1a72005e..cb17028f 100644 --- a/chipcompiler/docs/ecc-cli-dev.cn.md +++ b/chipcompiler/docs/ecc-cli-dev.cn.md @@ -257,7 +257,7 @@ ecc --help # 验证 doctor / signoff / report 已列出 - **模块体积**:文件超过约 800 LoC 时新功能放新模块,不要继续堆([../../CLAUDE.md](../../CLAUDE.md) 第 6 节)。 - **Python 3+**:不用 `__future__`;最低版本看 `pyproject.toml` 的 `requires-python`。 - **测试放置**按所有权边界;优先整对象比较;不为静态定义的值写测试;不为已删除的逻辑保留负向测试。 -- **代码评审**必须执行 [review-guidelines.md](review-guidelines.md) 的附加标准。 +- **代码评审**必须执行 [review-guidelines.md](../../docs/review-guidelines.md) 的附加标准。 - `uv.lock` 是依赖事实源;`requirements_lock.txt` 自动生成且被 gitignore。 - ECC-Tools 在代码里的工具标识是 `"ecc"`(不是 `"ecc-tools"`);每个工具模块需实现 `is_eda_exist / build_step / run_step`;步骤在 `multiprocessing.Process` 中执行,状态持久化在 `workspace.flow.json`。 - 依赖安装后 `ecc` 以 editable 方式生效,改源码下次导入即生效,无需重装。 diff --git a/chipcompiler/docs/ecc-cli-dev.en.md b/chipcompiler/docs/ecc-cli-dev.en.md index 69199631..52282a41 100644 --- a/chipcompiler/docs/ecc-cli-dev.en.md +++ b/chipcompiler/docs/ecc-cli-dev.en.md @@ -257,7 +257,7 @@ To roll back to the official release, re-run the [README](../../README.md#instal - **Module size**: once a file exceeds roughly 800 LoC, put new functionality in a new module instead of growing it (repository CLAUDE.md section 6). - **Python 3+**: do not use `__future__`; check `requires-python` in `pyproject.toml` for the minimum version. - **Test placement** follows ownership boundaries; prefer whole-object comparisons; do not write tests for statically defined values; do not keep negative tests for removed logic. -- **Code review** must enforce the additional standards in [review-guidelines.md](review-guidelines.md). +- **Code review** must enforce the additional standards in [review-guidelines.md](../../docs/review-guidelines.md). - `uv.lock` is the source of truth for dependencies; `requirements_lock.txt` is auto-generated and gitignored. - ECC-Tools' tool identifier in code is `"ecc"` (not `"ecc-tools"`); every tool module must implement `is_eda_exist / build_step / run_step`; steps execute in `multiprocessing.Process` and state persists in `workspace.flow.json`. - After installing dependencies, `ecc` is editable — source changes take effect on the next import, no reinstall needed. diff --git a/chipcompiler/docs/ecc-cli-tutorial.cn.md b/chipcompiler/docs/ecc-cli-tutorial.cn.md index 974e955e..572dadfd 100644 --- a/chipcompiler/docs/ecc-cli-tutorial.cn.md +++ b/chipcompiler/docs/ecc-cli-tutorial.cn.md @@ -1,6 +1,6 @@ # ECC CLI 入门教程:从零跑通 RTL → Harden 并产出签核包 -本教程面向第一次接触 ECC 的用户:从一台只有 Linux 系统的机器开始,安装 `ecc` 命令行工具,把一个 Verilog RTL 设计([gcd](examples/gcd/gcd.v),最大公约数计算单元)一路跑完 **综合 → 布局布线 → 物理验证 → 逻辑等价性检查(LEC)→ 时序签核 → Harden** 全流程,最终拿到: +本教程面向第一次接触 ECC 的用户:从一台只有 Linux 系统的机器开始,安装 `ecc` 命令行工具,把一个 Verilog RTL 设计([gcd](../../docs/examples/gcd/gcd.v),最大公约数计算单元)一路跑完 **综合 → 布局布线 → 物理验证 → 逻辑等价性检查(LEC)→ 时序签核 → Harden** 全流程,最终拿到: - **Harden 交付物**:GDS 版图、抽象 LEF、时序 LIB、版图快照 PNG; - **签核包** `gcd_signoff_package.tar.gz`(含 RTL/配置/交付物/LEC 证明/报告等 300+ 文件); @@ -184,7 +184,7 @@ curl -fL -o rtl/gcd.v \ # cp /path/to/ecc/docs/examples/gcd/gcd.v rtl/ ``` -多文件设计请改用 filelist(`rtl = ["rtl/filelist.f"]`),语法见 [examples/gcd/README.md](examples/gcd/README.md#using-filelist) 与 [filelist 语法](specification/filelist-grammar.md)。 +多文件设计请改用 filelist(`rtl = ["rtl/filelist.f"]`),语法见 [examples/gcd/README.md](../../docs/examples/gcd/README.md#using-filelist) 与 [filelist 语法](../../docs/specification/filelist-grammar.md)。 ### 3.3 认识 ecc.toml @@ -713,10 +713,10 @@ ecc config --plain # 项目级配置(键值 + 解析后绝对路径) ## 8. 下一步 -- 换你自己的设计:改 `ecc.toml` 的 `top`/`rtl`/`clock_port`/`frequency_mhz`,多文件用 [filelist](examples/gcd/README.md#using-filelist); +- 换你自己的设计:改 `ecc.toml` 的 `top`/`rtl`/`clock_port`/`frequency_mhz`,多文件用 [filelist](../../docs/examples/gcd/README.md#using-filelist); - 了解 preset 差异:`rtl2gds`(完整 15 步综合到 Harden 链,含综合级 LEC)、`syn_sta`(仅综合)、`synthesis_lec`(综合 + LEC,两步); - 全部命令细节见 **[ECC CLI 用户指南](ecc-cli-ug.cn.md)**;CLI 扩展开发见 [ecc-cli-dev.cn.md](ecc-cli-dev.cn.md); -- 用 Python API 直接编排 flow(`EngineFlow`)见 [examples/gcd/ics55flow.py](examples/gcd/ics55flow.py)。 +- 用 Python API 直接编排 flow(`EngineFlow`)见 [examples/gcd/ics55flow.py](../../docs/examples/gcd/ics55flow.py)。 --- diff --git a/chipcompiler/docs/ecc-cli-tutorial.en.md b/chipcompiler/docs/ecc-cli-tutorial.en.md index 7d7aeb0a..4f756876 100644 --- a/chipcompiler/docs/ecc-cli-tutorial.en.md +++ b/chipcompiler/docs/ecc-cli-tutorial.en.md @@ -1,6 +1,6 @@ # ECC CLI Tutorial: From Zero to RTL → Harden with a Signoff Package -This tutorial is for first-time ECC users: starting from a bare Linux machine, install the `ecc` command-line tool and drive a Verilog RTL design ([gcd](examples/gcd/gcd.v), a greatest-common-divisor unit) through the full **synthesis → place & route → physical verification → logic equivalence check (LEC) → timing signoff → Harden** flow, ending up with: +This tutorial is for first-time ECC users: starting from a bare Linux machine, install the `ecc` command-line tool and drive a Verilog RTL design ([gcd](../../docs/examples/gcd/gcd.v), a greatest-common-divisor unit) through the full **synthesis → place & route → physical verification → logic equivalence check (LEC) → timing signoff → Harden** flow, ending up with: - **Harden deliverables**: GDS layout, abstract LEF, timing LIB, and a layout snapshot PNG; - A **signoff package** `gcd_signoff_package.tar.gz` (300+ files: RTL / configs / deliverables / LEC proof / reports); @@ -185,7 +185,7 @@ curl -fL -o rtl/gcd.v \ # cp /path/to/ecc/docs/examples/gcd/gcd.v rtl/ ``` -For multi-file designs, switch to a filelist (`rtl = ["rtl/filelist.f"]`); see [examples/gcd/README.md](examples/gcd/README.md#using-filelist) and the [filelist grammar](specification/filelist-grammar.md). +For multi-file designs, switch to a filelist (`rtl = ["rtl/filelist.f"]`); see [examples/gcd/README.md](../../docs/examples/gcd/README.md#using-filelist) and the [filelist grammar](../../docs/specification/filelist-grammar.md). ### 3.3 Understanding ecc.toml @@ -714,10 +714,10 @@ ecc config --plain # project-level config (key=value + resolved absolute pa ## 8. Next Steps -- Try your own design: edit `top`/`rtl`/`clock_port`/`frequency_mhz` in `ecc.toml`; use a [filelist](examples/gcd/README.md#using-filelist) for multi-file designs; +- Try your own design: edit `top`/`rtl`/`clock_port`/`frequency_mhz` in `ecc.toml`; use a [filelist](../../docs/examples/gcd/README.md#using-filelist) for multi-file designs; - Preset differences: `rtl2gds` (the complete 15-step synthesis-to-Harden chain, including synthesis-level LEC), `syn_sta` (synthesis only), and `synthesis_lec` (synthesis + LEC, two steps); - Full command details in the **[ECC CLI User Guide](ecc-cli-ug.en.md)**; extending the CLI is covered in [ecc-cli-dev.en.md](ecc-cli-dev.en.md); -- Driving the flow directly via the Python API (`EngineFlow`): [examples/gcd/ics55flow.py](examples/gcd/ics55flow.py). +- Driving the flow directly via the Python API (`EngineFlow`): [examples/gcd/ics55flow.py](../../docs/examples/gcd/ics55flow.py). --- diff --git a/chipcompiler/docs/ecc-cli-ug.cn.md b/chipcompiler/docs/ecc-cli-ug.cn.md index 893278c9..f25a51b1 100644 --- a/chipcompiler/docs/ecc-cli-ug.cn.md +++ b/chipcompiler/docs/ecc-cli-ug.cn.md @@ -4,7 +4,7 @@ - 源码位置:[chipcompiler/cli/](../../chipcompiler/cli/) - 命令扩展开发方式见同目录 [ecc-cli-dev.cn.md](ecc-cli-dev.cn.md) -- RPC sidecar 协议详见 [workspace-cli.md](workspace-cli.md) +- RPC sidecar 协议详见 [workspace-cli.md](../../docs/workspace-cli.md) ## 0. 调用方式 @@ -960,7 +960,7 @@ $ ecc report step drc --section analysis ecc rpc serve --stdio [--persistent-db] ``` -供 GUI 等前端使用的 JSON-RPC 2.0 服务,`Content-Length` 帧封装于 stdio。`--persistent-db` 额外开放 `db.ensure` / `db.release` 与 `layout.edit.*` / `floorplan.edit.*` 系列方法。握手与调用示例(完整方法列表和参数见 [workspace-cli.md](workspace-cli.md)): +供 GUI 等前端使用的 JSON-RPC 2.0 服务,`Content-Length` 帧封装于 stdio。`--persistent-db` 额外开放 `db.ensure` / `db.release` 与 `layout.edit.*` / `floorplan.edit.*` 系列方法。握手与调用示例(完整方法列表和参数见 [workspace-cli.md](../../docs/workspace-cli.md)): ```console → {"jsonrpc":"2.0","method":"rpc.hello","params":{"version":1},"id":"hello-1"} diff --git a/chipcompiler/docs/ecc-cli-ug.en.md b/chipcompiler/docs/ecc-cli-ug.en.md index 1ac7a897..2603dbc1 100644 --- a/chipcompiler/docs/ecc-cli-ug.en.md +++ b/chipcompiler/docs/ecc-cli-ug.en.md @@ -4,7 +4,7 @@ - Source code: [chipcompiler/cli/](../../chipcompiler/cli/) - For how to extend the CLI with new commands, see [ecc-cli-dev.en.md](ecc-cli-dev.en.md) -- RPC sidecar protocol: [workspace-cli.md](workspace-cli.md) +- RPC sidecar protocol: [workspace-cli.md](../../docs/workspace-cli.md) ## 0. Invocation @@ -1008,7 +1008,7 @@ $ ecc report step drc --section analysis ecc rpc serve --stdio [--persistent-db] ``` -A JSON-RPC 2.0 service for front ends such as the GUI, framed with `Content-Length` over stdio. `--persistent-db` additionally exposes `db.ensure` / `db.release` plus the `layout.edit.*` / `floorplan.edit.*` method families. Handshake and call examples (full method list and parameters in [workspace-cli.md](workspace-cli.md)): +A JSON-RPC 2.0 service for front ends such as the GUI, framed with `Content-Length` over stdio. `--persistent-db` additionally exposes `db.ensure` / `db.release` plus the `layout.edit.*` / `floorplan.edit.*` method families. Handshake and call examples (full method list and parameters in [workspace-cli.md](../../docs/workspace-cli.md)): ```console → {"jsonrpc":"2.0","method":"rpc.hello","params":{"version":1},"id":"hello-1"} diff --git a/test/packaging/test_wheel_contents.py b/test/packaging/test_wheel_contents.py new file mode 100644 index 00000000..f23dfeff --- /dev/null +++ b/test/packaging/test_wheel_contents.py @@ -0,0 +1,24 @@ +import shutil +import subprocess +import zipfile +from pathlib import Path + +import pytest + +REPO_ROOT = Path(__file__).parents[2] + + +@pytest.mark.skipif(shutil.which("uv") is None, reason="uv is required to build the wheel") +def test_wheel_ships_all_doc_guides(tmp_path): + subprocess.run( + ["uv", "build", "--wheel", "--out-dir", str(tmp_path)], + cwd=REPO_ROOT, + check=True, + capture_output=True, + ) + + wheel = next(tmp_path.glob("ecc-*.whl")) + names = zipfile.ZipFile(wheel).namelist() + for stem in ("config", "ug", "tutorial", "dev"): + for lang in ("en", "cn"): + assert f"chipcompiler/docs/ecc-cli-{stem}.{lang}.md" in names From 9e24109771136d78ff26902e5af37afa891af218 Mon Sep 17 00:00:00 2001 From: Emin <me@emin.chat> Date: Mon, 7 Sep 2026 22:55:34 +0800 Subject: [PATCH 24/26] feat(cli): enable shell completion via --show-completion Turn on typer's built-in completion options on the root app and let completion requests through the empty-argv help shortcut, since they carry their arguments in _ECC_COMPLETE/COMP_WORDS env vars instead of argv. Document eval "$(ecc --show-completion)" in the en/cn guides, noting the --install-completion caveat on NixOS-style managed rc files. --- chipcompiler/cli/app.py | 6 ++-- chipcompiler/cli/core/apps.py | 4 +-- chipcompiler/docs/ecc-cli-ug.cn.md | 11 ++++++++ chipcompiler/docs/ecc-cli-ug.en.md | 11 ++++++++ test/cli/test_completion.py | 45 ++++++++++++++++++++++++++++++ 5 files changed, 73 insertions(+), 4 deletions(-) create mode 100644 test/cli/test_completion.py diff --git a/chipcompiler/cli/app.py b/chipcompiler/cli/app.py index bc228d90..5dad4684 100644 --- a/chipcompiler/cli/app.py +++ b/chipcompiler/cli/app.py @@ -1,4 +1,5 @@ import json +import os from collections.abc import Sequence from typing import Annotated @@ -18,7 +19,7 @@ from chipcompiler.cli.core.version_info import root_version_line, version_payload, version_text from chipcompiler.cli.inspection.tool_versions import tool_versions -app = create_app(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 @@ -94,7 +95,8 @@ def layout_image_cmd( def invoke_typer_app(argv: Sequence[str]) -> int: command = typer.main.get_command(app) - if not argv: + # 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 diff --git a/chipcompiler/cli/core/apps.py b/chipcompiler/cli/core/apps.py index 76109d68..4b4e9ced 100644 --- a/chipcompiler/cli/core/apps.py +++ b/chipcompiler/cli/core/apps.py @@ -1,9 +1,9 @@ import typer -def create_app(*, help: str) -> typer.Typer: +def create_app(*, help: str, add_completion: bool = False) -> typer.Typer: return typer.Typer( - add_completion=False, + add_completion=add_completion, no_args_is_help=True, rich_markup_mode="markdown", help=help, diff --git a/chipcompiler/docs/ecc-cli-ug.cn.md b/chipcompiler/docs/ecc-cli-ug.cn.md index f25a51b1..69c8e11d 100644 --- a/chipcompiler/docs/ecc-cli-ug.cn.md +++ b/chipcompiler/docs/ecc-cli-ug.cn.md @@ -129,6 +129,17 @@ ecc doc config --plain # 原始 markdown,逐字节输出 - 非法的主题/语言取值由参数校验拒绝(退出码 2)。 - 管道输出保留 unicode 渲染版式;`--plain` 原样输出原始 markdown,适合脚本处理。 +## 1.6. Shell 补全 + +`ecc` 内置 bash、zsh、fish、powershell 的 Shell 补全。打印激活脚本并加载到当前会话: + +```bash +eval "$(ecc --show-completion)" # 自动探测当前 shell +``` + +- 把该行写入 `~/.zshrc` / `~/.bashrc`(或 home-manager 的 `initExtra`)即可永久启用。zsh 需要先初始化 `compinit`。 +- 在 NixOS 等声明式管理的环境中,请用上面的 `eval` 方式而不是 `--install-completion`:后者会原地改写 `~/.zshrc` / `~/.bashrc`,对只读 rc 符号链接会失败,且与 home-manager 冲突。 + ## 2. version — 查看版本 ```bash diff --git a/chipcompiler/docs/ecc-cli-ug.en.md b/chipcompiler/docs/ecc-cli-ug.en.md index 2603dbc1..660df3c5 100644 --- a/chipcompiler/docs/ecc-cli-ug.en.md +++ b/chipcompiler/docs/ecc-cli-ug.en.md @@ -129,6 +129,17 @@ ecc doc config --plain # raw markdown, byte-for-byte - Invalid topic/language values are rejected by argument validation (exit 2). - Default output keeps the rendered unicode layout even when piped; `--plain` prints the raw markdown unchanged (script-friendly). +## 1.6. Shell completion + +`ecc` ships built-in shell completion for bash, zsh, fish, and powershell. Print the activation script and load it into the current session: + +```bash +eval "$(ecc --show-completion)" # auto-detects the current shell +``` + +- Add that line to `~/.zshrc` / `~/.bashrc` (or home-manager `initExtra`) to enable completion permanently. zsh requires `compinit` to be initialized first. +- On NixOS or other declaratively managed setups, prefer the `eval` line over `--install-completion`: the latter rewrites `~/.zshrc` / `~/.bashrc` in place, which fails on read-only rc symlinks and conflicts with home-manager. + ## 2. version — show versions ```bash diff --git a/test/cli/test_completion.py b/test/cli/test_completion.py new file mode 100644 index 00000000..ac981db0 --- /dev/null +++ b/test/cli/test_completion.py @@ -0,0 +1,45 @@ +from chipcompiler.cli import main as cli_main + + +def test_show_completion_prints_zsh_script(monkeypatch, capsys): + monkeypatch.setenv("_TYPER_COMPLETE_TEST_DISABLE_SHELL_DETECTION", "1") + + rc = cli_main.run(["--show-completion", "zsh"]) + + out = capsys.readouterr().out + assert rc == 0 + assert "#compdef ecc" in out + assert "_ECC_COMPLETE=complete_zsh" in out + + +def test_show_completion_prints_bash_script(monkeypatch, capsys): + monkeypatch.setenv("_TYPER_COMPLETE_TEST_DISABLE_SHELL_DETECTION", "1") + + rc = cli_main.run(["--show-completion", "bash"]) + + out = capsys.readouterr().out + assert rc == 0 + assert "complete -o default -F _ecc_completion ecc" in out + assert "_ECC_COMPLETE=complete_bash" in out + + +def test_completion_request_with_empty_argv_returns_candidates(monkeypatch, capsys): + monkeypatch.setenv("_ECC_COMPLETE", "complete_bash") + monkeypatch.setenv("COMP_WORDS", "ecc vers") + monkeypatch.setenv("COMP_CWORD", "1") + + rc = cli_main.run([]) + + out = capsys.readouterr().out + assert rc == 0 + assert out.strip() == "version" + + +def test_empty_argv_without_completion_env_still_shows_help(monkeypatch, capsys): + monkeypatch.delenv("_ECC_COMPLETE", raising=False) + + rc = cli_main.run([]) + + out = capsys.readouterr() + assert rc == 1 + assert "Commands" in out.out + out.err From 1be57d680381804f7d54e390b74f84e1cfebfb70 Mon Sep 17 00:00:00 2001 From: Emin <me@emin.chat> Date: Mon, 7 Sep 2026 23:07:11 +0800 Subject: [PATCH 25/26] docs(cli): collapse exhaustive param listings in the config guide Section 15 repeated one 'ecc param set KEY VALUE' line for every tunable field (86 for place.*, 24 for floorplan.*), duplicating what 'ecc param list --step STEP' already prints. Keep a few representative examples per step and defer the full enumeration to the CLI; the field tables in sections 2-14 remain the complete reference. --- chipcompiler/docs/ecc-cli-config.cn.md | 131 +------------------------ chipcompiler/docs/ecc-cli-config.en.md | 131 +------------------------ 2 files changed, 8 insertions(+), 254 deletions(-) diff --git a/chipcompiler/docs/ecc-cli-config.cn.md b/chipcompiler/docs/ecc-cli-config.cn.md index 6a8dd536..1114bda0 100644 --- a/chipcompiler/docs/ecc-cli-config.cn.md +++ b/chipcompiler/docs/ecc-cli-config.cn.md @@ -503,7 +503,7 @@ ics55 的 corner 命名:`Cworst/Cbest`=电容最差/最好,`RCworst/RCbest`= ## 15. 各步骤的 ECC CLI 配置命令 -以下清单将前文的配置字段对应到可执行的 ECC CLI 命令。`VALUE` 是待替换的值占位符;列表和对象必须传入 JSON 字面量,例如 `ecc param set cts.routing_layer '[4, 5]'`。命令默认写入项目的 `ecc.toml`;要修改已创建的 workspace,请在命令末尾追加 `--workspace NAME`(PDK 路径参数除外,见 §1.4)。先执行列出的 `ecc param list --step STEP` 可查看当前版本完整的可调字段、默认值和约束。 +所有字段的设置方式相同——`ecc param set KEY VALUE`——因此每个步骤只给出少量代表性示例;执行列出的 `ecc param list --step STEP` 可查看当前版本完整的可调字段、默认值和约束。`VALUE` 是待替换的值占位符;列表和对象必须传入 JSON 字面量,例如 `ecc param set cts.routing_layer '[4, 5]'`。命令默认写入项目的 `ecc.toml`;要修改已创建的 workspace,请在命令末尾追加 `--workspace NAME`(PDK 路径参数除外,见 §1.4)。 ### 15.1 公共 db 配置与 PDK 路径 @@ -511,12 +511,7 @@ ics55 的 corner 命名:`Cworst/Cbest`=电容最差/最好,`RCworst/RCbest`= ```bash ecc param list --step pdk -ecc param set pdk.tech VALUE -ecc param set pdk.lefs VALUE -ecc param set pdk.libs VALUE -ecc param set pdk.mapping_file VALUE -ecc param set pdk.sdc VALUE -ecc param set pdk.spef VALUE +ecc param set pdk.tech VALUE # pdk.lefs、pdk.libs、pdk.mapping_file、pdk.sdc、pdk.spef 用法相同 ``` `pdk.root` 使用 `ecc pdk set-root PATH`,不属于 `ecc param`。绕线首层由 routing 步骤的 `route.bottom_layer` 设置,见 §15.6。 @@ -532,34 +527,13 @@ ecc param set design.frequency_mhz VALUE ### 15.3 floorplan -`temp_directory_path` 与 `macro_location_path` 为流程生成/保护路径,不能通过 CLI 设置;其余已审核的 `floorplan_ecc.json` 字段使用: +`temp_directory_path` 与 `macro_location_path` 为流程生成/保护路径,不能通过 CLI 设置;其余已审核的 `floorplan_ecc.json` 字段用法相同,例如: ```bash ecc param list --step floorplan ecc param set floorplan.core_util VALUE -ecc param set floorplan.core_margin VALUE -ecc param set floorplan.aspect_ratio VALUE -ecc param set floorplan.die_builder.die_size.width_micron VALUE -ecc param set floorplan.die_builder.die_size.height_micron VALUE -ecc param set floorplan.die_builder.mode VALUE -ecc param set floorplan.die_builder.site_name VALUE -ecc param set floorplan.ifp.thread_number VALUE -ecc param set floorplan.io_placer.io_layer_list VALUE -ecc param set floorplan.macro_placer.macro_placement_halo VALUE -ecc param set floorplan.macro_placer.macro_routing_halo VALUE -ecc param set floorplan.pdn_generator.global_connect VALUE -ecc param set floorplan.pdn_generator.rail VALUE -ecc param set floorplan.pdn_generator.stripe VALUE -ecc param set floorplan.pdn_generator.connect_layers VALUE -ecc param set floorplan.phy_placer.well_tap.cell_name VALUE +ecc param set floorplan.die_builder.die_size.width_micron VALUE # 嵌套字段使用点路径 ecc param set floorplan.phy_placer.well_tap.distance_micron VALUE -ecc param set floorplan.phy_placer.side_endcap.left_cell_name VALUE -ecc param set floorplan.phy_placer.side_endcap.right_cell_name VALUE -ecc param set floorplan.phy_placer.edge_endcap.top_cell_name_list VALUE -ecc param set floorplan.phy_placer.edge_endcap.bottom_cell_name_list VALUE -ecc param set floorplan.phy_placer.boundary_tap.top_cell_name_list VALUE -ecc param set floorplan.phy_placer.boundary_tap.bottom_cell_name_list VALUE -ecc param set floorplan.phy_placer.boundary_tap.rule_micron VALUE ``` ### 15.4 placement / legalization @@ -569,91 +543,7 @@ placement 与 legalization 共用 `dreamplace_ecc.json`,因此使用同一组 ```bash ecc param list --step placement ecc param set place.target_density VALUE -ecc param set place.target_overflow VALUE -ecc param set place.cell_padding_x VALUE -ecc param set place.routability_opt VALUE -ecc param set place.RePlAce_LOWER_PCOF VALUE -ecc param set place.RePlAce_UPPER_PCOF VALUE -ecc param set place.RePlAce_ref_hpwl VALUE -ecc param set place.RePlAce_skip_energy_flag VALUE -ecc param set place.adjust_nctugr_area_flag VALUE -ecc param set place.adjust_pin_area_flag VALUE -ecc param set place.adjust_rudy_area_flag VALUE -ecc param set place.area_adjust_stop_ratio VALUE -ecc param set place.auto_adjust_bins VALUE -ecc param set place.bndry_padding_x VALUE -ecc param set place.bndry_padding_y VALUE -ecc param set place.density_weight VALUE -ecc param set place.detailed_place_command VALUE -ecc param set place.detailed_place_engine VALUE -ecc param set place.detailed_place_flag VALUE -ecc param set place.deterministic_flag VALUE -ecc param set place.differentiable_timing_obj VALUE -ecc param set place.dtype VALUE -ecc param set place.dump_global_place_solution_flag VALUE -ecc param set place.dump_legalize_solution_flag VALUE -ecc param set place.enable_fillers VALUE -ecc param set place.enable_net_weighting VALUE -ecc param set place.evaluate_pl VALUE -ecc param set place.gamma VALUE -ecc param set place.get_congestion_map VALUE -ecc param set place.global_place_flag VALUE -ecc param set place.global_place_stages VALUE -ecc param set place.gp_noise_ratio VALUE -ecc param set place.gpu VALUE -ecc param set place.gpu_id VALUE -ecc param set place.ignore_net_degree VALUE -ecc param set place.ignore_net_weight VALUE -ecc param set place.init_loc_perc_x VALUE -ecc param set place.init_loc_perc_y VALUE -ecc param set place.legalize_flag VALUE -ecc param set place.macro_halo_x VALUE -ecc param set place.macro_halo_y VALUE -ecc param set place.macro_overlap_flag VALUE -ecc param set place.macro_overlap_mult_weight VALUE -ecc param set place.macro_overlap_weight VALUE -ecc param set place.macro_pin_halo_x VALUE -ecc param set place.macro_pin_halo_y VALUE -ecc param set place.macro_place_flag VALUE -ecc param set place.max_net_weight VALUE -ecc param set place.max_num_area_adjust VALUE -ecc param set place.max_pin_opt_adjust_rate VALUE -ecc param set place.max_route_opt_adjust_rate VALUE -ecc param set place.momentum_decay_factor VALUE -ecc param set place.net_weighting_scheme VALUE -ecc param set place.node_area_adjust_overflow VALUE -ecc param set place.num_bins_x VALUE -ecc param set place.num_bins_y VALUE ecc param set place.num_threads VALUE -ecc param set place.pin2pin_accumulate_weight VALUE -ecc param set place.pin2pin_max_weight VALUE -ecc param set place.pin2pin_min_weight VALUE -ecc param set place.pin2pin_net_weighting VALUE -ecc param set place.pin2pin_weight VALUE -ecc param set place.pin_area_adjust_stop_ratio VALUE -ecc param set place.pin_density VALUE -ecc param set place.pin_stretch_ratio VALUE -ecc param set place.plot_flag VALUE -ecc param set place.random_center_init_flag VALUE -ecc param set place.random_seed VALUE -ecc param set place.risa_weights VALUE -ecc param set place.route_area_adjust_stop_ratio VALUE -ecc param set place.route_info_input VALUE -ecc param set place.route_num_bins_x VALUE -ecc param set place.route_num_bins_y VALUE -ecc param set place.route_opt_adjust_exponent VALUE -ecc param set place.scale_factor VALUE -ecc param set place.shift_factor VALUE -ecc param set place.sort_nets_by_degree VALUE -ecc param set place.start_iter VALUE -ecc param set place.timing_eval_flag VALUE -ecc param set place.timing_opt_flag VALUE -ecc param set place.two_stage_density_scaler VALUE -ecc param set place.unit_horizontal_capacity VALUE -ecc param set place.unit_pin_capacity VALUE -ecc param set place.unit_vertical_capacity VALUE -ecc param set place.use_bb VALUE -ecc param set place.with_sta VALUE ``` ### 15.5 timing optimization @@ -666,26 +556,13 @@ Sizer 没有专属 `ecc param` schema。其内部 DreamPlace 合法化使用 §1 ecc param list --step cts ecc param set cts.max_fanout VALUE ecc param set cts.skew_bound VALUE -ecc param set cts.max_buf_tran VALUE -ecc param set cts.root_input_slew VALUE -ecc param set cts.max_sink_tran VALUE -ecc param set cts.max_cap VALUE -ecc param set cts.max_length VALUE -ecc param set cts.wirelength_iterations VALUE -ecc param set cts.slew_steps VALUE -ecc param set cts.cap_steps VALUE ecc param set cts.routing_layer VALUE ecc param set cts.buffer_type VALUE -ecc param set cts.use_netlist VALUE -ecc param set cts.net_list VALUE ecc param list --step routing ecc param set route.bottom_layer VALUE ecc param set route.top_layer VALUE ecc param set route.RT.-thread_number VALUE -ecc param set route.RT.-enable_timing VALUE -ecc param set route.RT.-output_csv VALUE -ecc param set route.RT.-output_inter_result VALUE ``` `route_ecc.json` 的临时目录由步骤调度生成,不能通过 CLI 设置。 diff --git a/chipcompiler/docs/ecc-cli-config.en.md b/chipcompiler/docs/ecc-cli-config.en.md index 79eb1fdc..d9dcc275 100644 --- a/chipcompiler/docs/ecc-cli-config.en.md +++ b/chipcompiler/docs/ecc-cli-config.en.md @@ -501,7 +501,7 @@ No step-specific configuration file: it reuses `db_ecc.json` to locate its input ## 15. ECC CLI configuration commands by step -The following lists map the preceding configuration fields to executable ECC CLI commands. `VALUE` is a value placeholder; lists and objects must be passed as JSON literals, for example `ecc param set cts.routing_layer '[4, 5]'`. Commands write to the project `ecc.toml` by default; to change an existing workspace, append `--workspace NAME` (except for PDK path parameters; see §1.4). Run the listed `ecc param list --step STEP` first to see all tunable fields, defaults, and constraints in the current version. +Every field is set the same way — `ecc param set KEY VALUE` — so each step below shows only a few representative keys; run the listed `ecc param list --step STEP` for the complete set of tunable fields, defaults, and constraints in the current version. `VALUE` is a value placeholder; lists and objects must be passed as JSON literals, for example `ecc param set cts.routing_layer '[4, 5]'`. Commands write to the project `ecc.toml` by default; to change an existing workspace, append `--workspace NAME` (except for PDK path parameters; see §1.4). ### 15.1 Shared db configuration and PDK paths @@ -509,12 +509,7 @@ The DEF, netlist, and output paths in `db_ecc.json` are generated by step schedu ```bash ecc param list --step pdk -ecc param set pdk.tech VALUE -ecc param set pdk.lefs VALUE -ecc param set pdk.libs VALUE -ecc param set pdk.mapping_file VALUE -ecc param set pdk.sdc VALUE -ecc param set pdk.spef VALUE +ecc param set pdk.tech VALUE # likewise pdk.lefs, pdk.libs, pdk.mapping_file, pdk.sdc, pdk.spef ``` Use `ecc pdk set-root PATH` for `pdk.root`; it is not an `ecc param` field. The first routing layer is set by `route.bottom_layer` in the routing step; see §15.6. @@ -530,34 +525,13 @@ ecc param set design.frequency_mhz VALUE ### 15.3 floorplan -`temp_directory_path` and `macro_location_path` are generated/protected paths and cannot be set through the CLI. Use the following commands for the other reviewed `floorplan_ecc.json` fields: +`temp_directory_path` and `macro_location_path` are generated/protected paths and cannot be set through the CLI. The other reviewed `floorplan_ecc.json` fields are set the same way, for example: ```bash ecc param list --step floorplan ecc param set floorplan.core_util VALUE -ecc param set floorplan.core_margin VALUE -ecc param set floorplan.aspect_ratio VALUE -ecc param set floorplan.die_builder.die_size.width_micron VALUE -ecc param set floorplan.die_builder.die_size.height_micron VALUE -ecc param set floorplan.die_builder.mode VALUE -ecc param set floorplan.die_builder.site_name VALUE -ecc param set floorplan.ifp.thread_number VALUE -ecc param set floorplan.io_placer.io_layer_list VALUE -ecc param set floorplan.macro_placer.macro_placement_halo VALUE -ecc param set floorplan.macro_placer.macro_routing_halo VALUE -ecc param set floorplan.pdn_generator.global_connect VALUE -ecc param set floorplan.pdn_generator.rail VALUE -ecc param set floorplan.pdn_generator.stripe VALUE -ecc param set floorplan.pdn_generator.connect_layers VALUE -ecc param set floorplan.phy_placer.well_tap.cell_name VALUE +ecc param set floorplan.die_builder.die_size.width_micron VALUE # nested fields use dot paths ecc param set floorplan.phy_placer.well_tap.distance_micron VALUE -ecc param set floorplan.phy_placer.side_endcap.left_cell_name VALUE -ecc param set floorplan.phy_placer.side_endcap.right_cell_name VALUE -ecc param set floorplan.phy_placer.edge_endcap.top_cell_name_list VALUE -ecc param set floorplan.phy_placer.edge_endcap.bottom_cell_name_list VALUE -ecc param set floorplan.phy_placer.boundary_tap.top_cell_name_list VALUE -ecc param set floorplan.phy_placer.boundary_tap.bottom_cell_name_list VALUE -ecc param set floorplan.phy_placer.boundary_tap.rule_micron VALUE ``` ### 15.4 placement / legalization @@ -567,91 +541,7 @@ Placement and legalization share `dreamplace_ecc.json`, so they use the same `pl ```bash ecc param list --step placement ecc param set place.target_density VALUE -ecc param set place.target_overflow VALUE -ecc param set place.cell_padding_x VALUE -ecc param set place.routability_opt VALUE -ecc param set place.RePlAce_LOWER_PCOF VALUE -ecc param set place.RePlAce_UPPER_PCOF VALUE -ecc param set place.RePlAce_ref_hpwl VALUE -ecc param set place.RePlAce_skip_energy_flag VALUE -ecc param set place.adjust_nctugr_area_flag VALUE -ecc param set place.adjust_pin_area_flag VALUE -ecc param set place.adjust_rudy_area_flag VALUE -ecc param set place.area_adjust_stop_ratio VALUE -ecc param set place.auto_adjust_bins VALUE -ecc param set place.bndry_padding_x VALUE -ecc param set place.bndry_padding_y VALUE -ecc param set place.density_weight VALUE -ecc param set place.detailed_place_command VALUE -ecc param set place.detailed_place_engine VALUE -ecc param set place.detailed_place_flag VALUE -ecc param set place.deterministic_flag VALUE -ecc param set place.differentiable_timing_obj VALUE -ecc param set place.dtype VALUE -ecc param set place.dump_global_place_solution_flag VALUE -ecc param set place.dump_legalize_solution_flag VALUE -ecc param set place.enable_fillers VALUE -ecc param set place.enable_net_weighting VALUE -ecc param set place.evaluate_pl VALUE -ecc param set place.gamma VALUE -ecc param set place.get_congestion_map VALUE -ecc param set place.global_place_flag VALUE -ecc param set place.global_place_stages VALUE -ecc param set place.gp_noise_ratio VALUE -ecc param set place.gpu VALUE -ecc param set place.gpu_id VALUE -ecc param set place.ignore_net_degree VALUE -ecc param set place.ignore_net_weight VALUE -ecc param set place.init_loc_perc_x VALUE -ecc param set place.init_loc_perc_y VALUE -ecc param set place.legalize_flag VALUE -ecc param set place.macro_halo_x VALUE -ecc param set place.macro_halo_y VALUE -ecc param set place.macro_overlap_flag VALUE -ecc param set place.macro_overlap_mult_weight VALUE -ecc param set place.macro_overlap_weight VALUE -ecc param set place.macro_pin_halo_x VALUE -ecc param set place.macro_pin_halo_y VALUE -ecc param set place.macro_place_flag VALUE -ecc param set place.max_net_weight VALUE -ecc param set place.max_num_area_adjust VALUE -ecc param set place.max_pin_opt_adjust_rate VALUE -ecc param set place.max_route_opt_adjust_rate VALUE -ecc param set place.momentum_decay_factor VALUE -ecc param set place.net_weighting_scheme VALUE -ecc param set place.node_area_adjust_overflow VALUE -ecc param set place.num_bins_x VALUE -ecc param set place.num_bins_y VALUE ecc param set place.num_threads VALUE -ecc param set place.pin2pin_accumulate_weight VALUE -ecc param set place.pin2pin_max_weight VALUE -ecc param set place.pin2pin_min_weight VALUE -ecc param set place.pin2pin_net_weighting VALUE -ecc param set place.pin2pin_weight VALUE -ecc param set place.pin_area_adjust_stop_ratio VALUE -ecc param set place.pin_density VALUE -ecc param set place.pin_stretch_ratio VALUE -ecc param set place.plot_flag VALUE -ecc param set place.random_center_init_flag VALUE -ecc param set place.random_seed VALUE -ecc param set place.risa_weights VALUE -ecc param set place.route_area_adjust_stop_ratio VALUE -ecc param set place.route_info_input VALUE -ecc param set place.route_num_bins_x VALUE -ecc param set place.route_num_bins_y VALUE -ecc param set place.route_opt_adjust_exponent VALUE -ecc param set place.scale_factor VALUE -ecc param set place.shift_factor VALUE -ecc param set place.sort_nets_by_degree VALUE -ecc param set place.start_iter VALUE -ecc param set place.timing_eval_flag VALUE -ecc param set place.timing_opt_flag VALUE -ecc param set place.two_stage_density_scaler VALUE -ecc param set place.unit_horizontal_capacity VALUE -ecc param set place.unit_pin_capacity VALUE -ecc param set place.unit_vertical_capacity VALUE -ecc param set place.use_bb VALUE -ecc param set place.with_sta VALUE ``` ### 15.5 timing optimization @@ -664,26 +554,13 @@ Sizer has no dedicated `ecc param` schema. Its internal DreamPlace legalization ecc param list --step cts ecc param set cts.max_fanout VALUE ecc param set cts.skew_bound VALUE -ecc param set cts.max_buf_tran VALUE -ecc param set cts.root_input_slew VALUE -ecc param set cts.max_sink_tran VALUE -ecc param set cts.max_cap VALUE -ecc param set cts.max_length VALUE -ecc param set cts.wirelength_iterations VALUE -ecc param set cts.slew_steps VALUE -ecc param set cts.cap_steps VALUE ecc param set cts.routing_layer VALUE ecc param set cts.buffer_type VALUE -ecc param set cts.use_netlist VALUE -ecc param set cts.net_list VALUE ecc param list --step routing ecc param set route.bottom_layer VALUE ecc param set route.top_layer VALUE ecc param set route.RT.-thread_number VALUE -ecc param set route.RT.-enable_timing VALUE -ecc param set route.RT.-output_csv VALUE -ecc param set route.RT.-output_inter_result VALUE ``` The temporary directory in `route_ecc.json` is generated by step scheduling and cannot be set through the CLI. From e13fbd41bf7053ef5377eb13e7ff0624ecca04a9 Mon Sep 17 00:00:00 2001 From: Emin <me@emin.chat> Date: Tue, 8 Sep 2026 11:23:33 +0800 Subject: [PATCH 26/26] test(cli): stabilize help rendering under CI --- test/cli/conftest.py | 5 +++++ test/cli/test_help_rendering.py | 11 +++++++++++ 2 files changed, 16 insertions(+) diff --git a/test/cli/conftest.py b/test/cli/conftest.py index 2a0d6248..fd84107b 100644 --- a/test/cli/conftest.py +++ b/test/cli/conftest.py @@ -153,6 +153,11 @@ def _stub_run_preflight(monkeypatch): ) +@pytest.fixture(autouse=True) +def _disable_typer_terminal_forcing(monkeypatch): + monkeypatch.setattr("typer.rich_utils.FORCE_TERMINAL", False) + + @pytest.fixture(name="create_cli_project") def create_cli_project_fixture(tmp_path): def factory(name="gcd", pdk_root=None, freq=100.0): diff --git a/test/cli/test_help_rendering.py b/test/cli/test_help_rendering.py index fe43ba5d..c8fc586e 100644 --- a/test/cli/test_help_rendering.py +++ b/test/cli/test_help_rendering.py @@ -26,6 +26,17 @@ def test_help_renders_for_every_command(path, capsys): assert "\x1b[" not in captured.out +def test_help_keeps_styles_when_color_is_forced(monkeypatch, capsys): + monkeypatch.setattr("typer.rich_utils.FORCE_TERMINAL", True) + + rc = cli_main.run(["--help"]) + + captured = capsys.readouterr() + assert rc == 0 + assert "\x1b[" in captured.out + assert not captured.err + + def test_root_command_list_keeps_one_line_summaries(capsys): rc = cli_main.run(["--help"])