tan-cli 0.7.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tan/__init__.py +0 -0
- tan/__main__.py +120 -0
- tan/cli.py +939 -0
- tan/command_table.py +265 -0
- tan/commands/__init__.py +8 -0
- tan/commands/bootstrap_cmd.py +4856 -0
- tan/commands/bootstrap_patches.py +164 -0
- tan/commands/bootstrap_toolchain_fetch.py +155 -0
- tan/commands/bootstrap_toolchain_pin.py +36 -0
- tan/commands/build/__init__.py +1 -0
- tan/commands/build/configure_inputs.py +277 -0
- tan/commands/build/execute.py +1852 -0
- tan/commands/build/host_python.py +59 -0
- tan/commands/build/link_stale.py +78 -0
- tan/commands/build/manifest.py +428 -0
- tan/commands/build/materialise.py +81 -0
- tan/commands/build/token_substitution.py +381 -0
- tan/commands/build/toolchain.py +482 -0
- tan/commands/build/user_defines_wipe.py +115 -0
- tan/commands/build/workspace_patches.py +65 -0
- tan/commands/build_cmd.py +2236 -0
- tan/commands/build_output.py +331 -0
- tan/commands/clean_cmd.py +1183 -0
- tan/commands/completion_cmd.py +786 -0
- tan/commands/debug_config_cmd.py +2425 -0
- tan/commands/diff_cmd.py +940 -0
- tan/commands/doctor_cmd.py +4767 -0
- tan/commands/examples_cmd.py +644 -0
- tan/commands/explain_cmd.py +1319 -0
- tan/commands/faultdecode_cmd.py +931 -0
- tan/commands/flash_atoc_guard.py +240 -0
- tan/commands/flash_cmd.py +5731 -0
- tan/commands/flash_mram_guard.py +150 -0
- tan/commands/flash_ram.py +578 -0
- tan/commands/flash_raw.py +583 -0
- tan/commands/generate_cmd.py +1456 -0
- tan/commands/image_cmd.py +753 -0
- tan/commands/init_cmd.py +1740 -0
- tan/commands/inspect_cmd.py +377 -0
- tan/commands/kconfig_cmd.py +734 -0
- tan/commands/model_cmd.py +1514 -0
- tan/commands/model_device_cmd.py +351 -0
- tan/commands/model_device_live.py +234 -0
- tan/commands/model_host_cmd.py +430 -0
- tan/commands/model_zoo_cmd.py +363 -0
- tan/commands/monitor_actions.py +125 -0
- tan/commands/monitor_cmd.py +626 -0
- tan/commands/monitor_session.py +505 -0
- tan/commands/new_som_cmd.py +1522 -0
- tan/commands/pinmux_cmd.py +568 -0
- tan/commands/presets_cmd.py +1123 -0
- tan/commands/probe_cmd.py +421 -0
- tan/commands/reset_cmd.py +354 -0
- tan/commands/reset_confirm.py +168 -0
- tan/commands/run_cmd.py +637 -0
- tan/commands/scaffold_cmd.py +490 -0
- tan/commands/sdk_cmd.py +1723 -0
- tan/commands/size_cmd.py +1020 -0
- tan/commands/support_bundle_cmd.py +1073 -0
- tan/commands/trace_cmd.py +341 -0
- tan/commands/validate_cmd.py +2009 -0
- tan/commands/west_forward_cmd.py +736 -0
- tan/commands/workspace_patch_check.py +296 -0
- tan/core/__init__.py +1 -0
- tan/core/alp_conf_pregen.py +100 -0
- tan/core/artifact_provenance.py +240 -0
- tan/core/atoc_guard.py +276 -0
- tan/core/atoc_guard_messages.py +232 -0
- tan/core/atoc_replacement.py +118 -0
- tan/core/atomic_write.py +178 -0
- tan/core/board_context.py +351 -0
- tan/core/board_diagnostic.py +111 -0
- tan/core/board_files.py +221 -0
- tan/core/board_validator.py +542 -0
- tan/core/board_validator_compat.py +196 -0
- tan/core/board_validator_run.py +129 -0
- tan/core/board_validator_schema.py +186 -0
- tan/core/board_validator_skew.py +46 -0
- tan/core/board_yaml_edit.py +295 -0
- tan/core/board_yaml_pos.py +118 -0
- tan/core/bootstrap.py +2572 -0
- tan/core/build_plan.py +625 -0
- tan/core/consent.py +104 -0
- tan/core/console_filter.py +245 -0
- tan/core/debug_launch.py +1803 -0
- tan/core/devicetree_lint.py +168 -0
- tan/core/dir_removal.py +229 -0
- tan/core/doctor_git.py +172 -0
- tan/core/doctor_libraries.py +253 -0
- tan/core/doctor_render.py +185 -0
- tan/core/doctor_scope.py +66 -0
- tan/core/doctor_setools.py +237 -0
- tan/core/doctor_stale.py +180 -0
- tan/core/document_guards.py +723 -0
- tan/core/dp_id.py +97 -0
- tan/core/elf_load.py +63 -0
- tan/core/error_catalog.py +171 -0
- tan/core/example_catalog.py +499 -0
- tan/core/example_facets.py +151 -0
- tan/core/faultdecode.py +521 -0
- tan/core/flash_plan.py +2501 -0
- tan/core/flow_d_report.py +381 -0
- tan/core/flow_d_uptime.py +164 -0
- tan/core/fs_confine.py +130 -0
- tan/core/global_flags.py +230 -0
- tan/core/host_python.py +210 -0
- tan/core/image_bundle.py +247 -0
- tan/core/inert.py +147 -0
- tan/core/jlink_binary.py +128 -0
- tan/core/jlink_probe.py +481 -0
- tan/core/jsonc_splice.py +404 -0
- tan/core/launch_provenance.py +257 -0
- tan/core/link_refusal.py +76 -0
- tan/core/metadata_schema.py +376 -0
- tan/core/model_check.py +227 -0
- tan/core/model_device.py +449 -0
- tan/core/model_doctor.py +198 -0
- tan/core/model_host.py +48 -0
- tan/core/model_list.py +112 -0
- tan/core/model_zoo.py +353 -0
- tan/core/module_template.py +340 -0
- tan/core/mram_link.py +87 -0
- tan/core/new_som_ospi.py +94 -0
- tan/core/os_class.py +159 -0
- tan/core/patch_paths.py +87 -0
- tan/core/pending.py +40 -0
- tan/core/plain_zephyr_plan.py +138 -0
- tan/core/plan_exec.py +373 -0
- tan/core/plan_tokens.py +600 -0
- tan/core/probe.py +155 -0
- tan/core/probe_plan.py +237 -0
- tan/core/proxy.py +168 -0
- tan/core/publish.py +31 -0
- tan/core/python_floor.py +178 -0
- tan/core/ram_run.py +694 -0
- tan/core/ram_watch.py +231 -0
- tan/core/raw_write.py +160 -0
- tan/core/reset_plan.py +56 -0
- tan/core/run.py +161 -0
- tan/core/scaffold.py +2203 -0
- tan/core/scaffold_selftest_identity.py +225 -0
- tan/core/sdk_default_registry.py +565 -0
- tan/core/sdk_discovery.py +996 -0
- tan/core/sdk_removal.py +465 -0
- tan/core/serial_actions.py +175 -0
- tan/core/serial_capture.py +185 -0
- tan/core/serial_url.py +45 -0
- tan/core/setools.py +608 -0
- tan/core/setools_scratch.py +243 -0
- tan/core/shapes.py +194 -0
- tan/core/size.py +610 -0
- tan/core/som_buildability.py +204 -0
- tan/core/som_schema_version.py +62 -0
- tan/core/subprocess_env.py +201 -0
- tan/core/sysbuild_image.py +94 -0
- tan/core/system_manifest.py +827 -0
- tan/core/text_layout.py +73 -0
- tan/core/timestamp.py +182 -0
- tan/core/tool_lookup.py +383 -0
- tan/core/toolchain_pin.py +117 -0
- tan/core/toolchain_provision.py +1224 -0
- tan/core/trusted_path.py +134 -0
- tan/core/uboot_breakin.py +154 -0
- tan/core/uri_reference.py +415 -0
- tan/core/user_defines.py +152 -0
- tan/core/venv.py +332 -0
- tan/core/west_patches.py +143 -0
- tan/core/west_workspace_refusal.py +159 -0
- tan/core/zephyr_env.py +124 -0
- tan/env.py +185 -0
- tan/envelope.py +647 -0
- tan/exit_codes.py +12 -0
- tan/lazy_typer.py +185 -0
- tan/model/__init__.py +2 -0
- tan/model/_gen_fixture.py +132 -0
- tan/model/adapters/__init__.py +116 -0
- tan/model/adapters/cpu.py +26 -0
- tan/model/adapters/deepx.py +94 -0
- tan/model/adapters/drpai.py +242 -0
- tan/model/adapters/ethos_u.py +951 -0
- tan/model/adapters/executorch.py +55 -0
- tan/model/analyze.py +694 -0
- tan/model/build.py +304 -0
- tan/model/check.py +682 -0
- tan/model/manifest.py +625 -0
- tan/model/measure.py +216 -0
- tan/model/npu_ops_validate.py +130 -0
- tan/model/package.py +125 -0
- tan/model/perf.py +635 -0
- tan/model/perf_apply.py +721 -0
- tan/model/prep.py +193 -0
- tan/model/sram_fit.py +502 -0
- tan/model/targets.py +374 -0
- tan/model/tensorio.py +287 -0
- tan/net.py +73 -0
- tan/output_format.py +213 -0
- tan/planner/__init__.py +219 -0
- tan/planner/aperture.py +202 -0
- tan/planner/buildplan.py +1092 -0
- tan/planner/camera_owner.py +192 -0
- tan/planner/cameras.py +132 -0
- tan/planner/carveout.py +755 -0
- tan/planner/cli.py +274 -0
- tan/planner/headers.py +315 -0
- tan/planner/kconfig.py +2435 -0
- tan/planner/kconfig_symbols.py +473 -0
- tan/planner/libraries.py +677 -0
- tan/planner/link_target.py +237 -0
- tan/planner/linux_ownership.py +216 -0
- tan/planner/loader.py +1525 -0
- tan/planner/manifest.py +116 -0
- tan/planner/memory.py +161 -0
- tan/planner/memregion.py +56 -0
- tan/planner/models.py +533 -0
- tan/planner/orchestrator.py +874 -0
- tan/planner/ownership.py +203 -0
- tan/planner/partition.py +1160 -0
- tan/planner/paths.py +32 -0
- tan/planner/plain_slice.py +139 -0
- tan/planner/project_emit/__init__.py +60 -0
- tan/planner/project_emit/bom_netlist.py +375 -0
- tan/planner/project_emit/dts.py +580 -0
- tan/planner/project_emit/hw_info.py +221 -0
- tan/planner/project_emit/native_sim.py +136 -0
- tan/planner/project_emit/west_libs.py +213 -0
- tan/planner/project_loader.py +264 -0
- tan/planner/sdk_capability.py +203 -0
- tan/planner/sdk_compat.py +395 -0
- tan/planner/secure.py +350 -0
- tan/planner/slugs.py +394 -0
- tan/planner/som_metadata.py +325 -0
- tan/planner/strict_loaders.py +131 -0
- tan/planner/template.py +1037 -0
- tan/planner/template_pins.py +535 -0
- tan/planner/template_rewrite.py +714 -0
- tan/planner/topology.py +208 -0
- tan/planner/validate.py +365 -0
- tan/planner/whole_device_alias.py +67 -0
- tan/planner/zephyr_board.py +3510 -0
- tan/planner_cli.py +97 -0
- tan/planner_emit.py +617 -0
- tan/planner_root.py +119 -0
- tan/soc_ref.py +58 -0
- tan/templates/__init__.py +34 -0
- tan/templates/vendored/MANIFEST.md +1862 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/CMakeLists.txt +8 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/README.md +112 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/board.yaml +38 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/boards/native_sim_native_64.conf +11 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/boards/native_sim_native_64.overlay +29 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/prj.conf +5 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/src/main.c +266 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/testcase.yaml +53 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/CMakeLists.txt +8 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/README.md +112 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/board.yaml +38 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/boards/native_sim_native_64.conf +11 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/boards/native_sim_native_64.overlay +29 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/prj.conf +5 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/src/main.c +266 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/testcase.yaml +53 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/CMakeLists.txt +10 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/README.md +75 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/board.yaml +38 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/prj.conf +8 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/src/cold_chain.c +371 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/src/cold_chain.h +178 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/src/main.c +373 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/testcase.yaml +62 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/CMakeLists.txt +10 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/README.md +75 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/board.yaml +37 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/prj.conf +8 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/src/cold_chain.c +371 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/src/cold_chain.h +178 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/src/main.c +373 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/testcase.yaml +62 -0
- tan/templates/vendored/iot/E1M-AEN801/CMakeLists.txt +14 -0
- tan/templates/vendored/iot/E1M-AEN801/README.md +103 -0
- tan/templates/vendored/iot/E1M-AEN801/board.yaml +69 -0
- tan/templates/vendored/iot/E1M-AEN801/prj.conf +15 -0
- tan/templates/vendored/iot/E1M-AEN801/src/cc3501e_bridge.c +196 -0
- tan/templates/vendored/iot/E1M-AEN801/src/cc3501e_bridge.h +173 -0
- tan/templates/vendored/iot/E1M-AEN801/src/main.c +262 -0
- tan/templates/vendored/iot/E1M-AEN801/testcase.yaml +68 -0
- tan/templates/vendored/minimal/E1M-AEN801/CMakeLists.txt +8 -0
- tan/templates/vendored/minimal/E1M-AEN801/README.md +108 -0
- tan/templates/vendored/minimal/E1M-AEN801/board.yaml +32 -0
- tan/templates/vendored/minimal/E1M-AEN801/prj.conf +8 -0
- tan/templates/vendored/minimal/E1M-AEN801/src/main.c +111 -0
- tan/templates/vendored/minimal/E1M-AEN801/testcase.yaml +49 -0
- tan/templates/vendored/minimal/E1M-V2N101/CMakeLists.txt +8 -0
- tan/templates/vendored/minimal/E1M-V2N101/README.md +108 -0
- tan/templates/vendored/minimal/E1M-V2N101/board.yaml +32 -0
- tan/templates/vendored/minimal/E1M-V2N101/prj.conf +8 -0
- tan/templates/vendored/minimal/E1M-V2N101/src/main.c +111 -0
- tan/templates/vendored/minimal/E1M-V2N101/testcase.yaml +49 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/CMakeLists.txt +8 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/README.md +120 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/board.yaml +57 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/boards/native_sim_native_64.overlay +40 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/peer/CMakeLists.txt +13 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/peer/main.c +171 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/peer/prj.conf +17 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/peer/testcase.yaml +23 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/prj.conf +22 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/src/main.c +185 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/testcase.yaml +26 -0
- tan/templates/vendored/sensor/E1M-AEN801/CMakeLists.txt +8 -0
- tan/templates/vendored/sensor/E1M-AEN801/README.md +112 -0
- tan/templates/vendored/sensor/E1M-AEN801/board.yaml +57 -0
- tan/templates/vendored/sensor/E1M-AEN801/boards/native_sim_native_64.conf +10 -0
- tan/templates/vendored/sensor/E1M-AEN801/boards/native_sim_native_64.overlay +28 -0
- tan/templates/vendored/sensor/E1M-AEN801/prj.conf +5 -0
- tan/templates/vendored/sensor/E1M-AEN801/src/main.c +186 -0
- tan/templates/vendored/sensor/E1M-AEN801/testcase.yaml +52 -0
- tan/templates/vendored/sensor/E1M-V2N101/CMakeLists.txt +8 -0
- tan/templates/vendored/sensor/E1M-V2N101/README.md +112 -0
- tan/templates/vendored/sensor/E1M-V2N101/board.yaml +57 -0
- tan/templates/vendored/sensor/E1M-V2N101/boards/native_sim_native_64.conf +10 -0
- tan/templates/vendored/sensor/E1M-V2N101/boards/native_sim_native_64.overlay +28 -0
- tan/templates/vendored/sensor/E1M-V2N101/prj.conf +5 -0
- tan/templates/vendored/sensor/E1M-V2N101/src/main.c +186 -0
- tan/templates/vendored/sensor/E1M-V2N101/testcase.yaml +52 -0
- tan/version.py +102 -0
- tan_cli-0.7.0.dist-info/METADATA +125 -0
- tan_cli-0.7.0.dist-info/RECORD +330 -0
- tan_cli-0.7.0.dist-info/WHEEL +5 -0
- tan_cli-0.7.0.dist-info/entry_points.txt +2 -0
- tan_cli-0.7.0.dist-info/top_level.txt +1 -0
tan/commands/size_cmd.py
ADDED
|
@@ -0,0 +1,1020 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""`tan size` -- measure each Zephyr slice's `zephyr.elf` FLASH/RAM and compare
|
|
3
|
+
it to the SoM memory budget resolved from SoC metadata.
|
|
4
|
+
|
|
5
|
+
Port of `crates/tan-cli/src/commands/size.rs`: the IO/subprocess half. Every
|
|
6
|
+
measurement / budget / classification / render *decision* is pure in
|
|
7
|
+
`tan.core.size`, so this module is only file reads, one subprocess, and the
|
|
8
|
+
envelope.
|
|
9
|
+
|
|
10
|
+
**No `--os`, no `--backend`.** A slice's runtime arrives already resolved in the
|
|
11
|
+
manifest (I-01); this command reads `os` and never chooses it.
|
|
12
|
+
|
|
13
|
+
**tan learns no hardware fact.** The budget is whatever `<sdk>/metadata/**` says
|
|
14
|
+
-- SKU, variant order code, SRAM bank names and `tcm_kb` are all read out of
|
|
15
|
+
those files at runtime and none is spelled here (I-26). The SDK is READ, never
|
|
16
|
+
executed: no `alp_project.py`, no `alp_orchestrate`, no `west` (I-32 and its
|
|
17
|
+
anti-pattern #22 -- shelling the SDK gives a command a checkout dependency it
|
|
18
|
+
does not have, and no gate catches the first one). A missing SDK is not fatal:
|
|
19
|
+
the budget resolves `unknown` and measurement still runs.
|
|
20
|
+
|
|
21
|
+
Divergences from the retired v0.4.1 oracle, all deliberate.
|
|
22
|
+
`tests/parity/test_image_size_oracle.py` pinned them until tan-cli#269 deleted
|
|
23
|
+
the oracle axis with `crates/`. They did not go unpinned: the nesting bullet is
|
|
24
|
+
held tan-side by `tests/commands/test_size_command.py`'s
|
|
25
|
+
`test_i18_nested_west_output_is_measured_not_reported_not_built` and
|
|
26
|
+
`test_the_un_nested_path_still_wins_when_both_exist`, and the candidate ORDER
|
|
27
|
+
by `tests/core/test_system_manifest.py`'s
|
|
28
|
+
`test_i18_nesting_is_probed_after_the_plain_path_never_before`. The size-tool
|
|
29
|
+
timeout below is the one that really is unpinned:
|
|
30
|
+
- a `zephyr.elf` (or `rom.json`/`ram.json`) that landed in west's nested
|
|
31
|
+
`<build_dir>/build/` is FOUND rather than reported `not-built`. That is I-18:
|
|
32
|
+
`west build` is emitted with no `-d`, and the shipped binary reconciles the
|
|
33
|
+
nesting at BUILD time (`resolve_zephyr_artefact` writes the resolved paths
|
|
34
|
+
into the manifest it rewrites). This port's `tan build` does not yet write
|
|
35
|
+
that manifest, so the reconciliation happens on the read side -- see
|
|
36
|
+
`tan.core.system_manifest`. The un-nested path is always tried first, so
|
|
37
|
+
every input the oracle measures is measured identically.
|
|
38
|
+
- the size tool gets a timeout. The oracle has none; a wedged
|
|
39
|
+
`arm-zephyr-eabi-size` would hang `tan size` forever.
|
|
40
|
+
- a PyYAML that is declared but missing anyway (a frozen artifact built from a
|
|
41
|
+
stale venv) is a coded `size.manifest-invalid`, not a traceback.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
from __future__ import annotations
|
|
45
|
+
|
|
46
|
+
import json
|
|
47
|
+
import os
|
|
48
|
+
import subprocess
|
|
49
|
+
from dataclasses import dataclass
|
|
50
|
+
from pathlib import Path
|
|
51
|
+
from typing import Any
|
|
52
|
+
|
|
53
|
+
import typer
|
|
54
|
+
|
|
55
|
+
from tan.core.shapes import is_file as _is_file
|
|
56
|
+
from tan.core.shapes import rejected_sdk_root_message
|
|
57
|
+
from tan.core.system_manifest import effective_build_root
|
|
58
|
+
from tan.commands.build_output import (
|
|
59
|
+
ManifestInvalid,
|
|
60
|
+
ManifestUnavailable,
|
|
61
|
+
ProjectContext,
|
|
62
|
+
load_manifest,
|
|
63
|
+
read_sdk_som_and_soc,
|
|
64
|
+
resolve_app_base,
|
|
65
|
+
resolve_build_root,
|
|
66
|
+
resolve_metadata_sdk_root,
|
|
67
|
+
resolve_project_context,
|
|
68
|
+
)
|
|
69
|
+
from tan.core.flash_plan import slice_should_flash
|
|
70
|
+
from tan.core.global_flags import accept_global_flags
|
|
71
|
+
from tan.core.metadata_schema import (
|
|
72
|
+
missing_schema_note,
|
|
73
|
+
soc_spec_schema_path,
|
|
74
|
+
som_preset_schema_path,
|
|
75
|
+
validate_document,
|
|
76
|
+
)
|
|
77
|
+
from tan.core.pending import is_pending_placeholder
|
|
78
|
+
from tan.core.som_schema_version import (
|
|
79
|
+
is_supported_som_schema_version,
|
|
80
|
+
skipped_presets_message,
|
|
81
|
+
)
|
|
82
|
+
from tan.core.sdk_discovery import sdk_resolution_issues
|
|
83
|
+
from tan.core.subprocess_env import spawn_env
|
|
84
|
+
from tan.core.size import (
|
|
85
|
+
MemoryBudget,
|
|
86
|
+
SliceSize,
|
|
87
|
+
budget_note_only,
|
|
88
|
+
build_size_report,
|
|
89
|
+
classify,
|
|
90
|
+
footprint_total,
|
|
91
|
+
over_budget_rows,
|
|
92
|
+
parse_berkeley_size,
|
|
93
|
+
render_table_lines,
|
|
94
|
+
resolve_budget,
|
|
95
|
+
resolve_variant,
|
|
96
|
+
sizes_from_elf_sections,
|
|
97
|
+
sram_banks,
|
|
98
|
+
unknown_budget_rows,
|
|
99
|
+
)
|
|
100
|
+
from tan.core.system_manifest import (
|
|
101
|
+
load_yaml_document,
|
|
102
|
+
slice_elf_candidates,
|
|
103
|
+
slice_footprint_dirs,
|
|
104
|
+
)
|
|
105
|
+
from tan.core.tool_lookup import resolve_tool
|
|
106
|
+
from tan.env import use_color
|
|
107
|
+
from tan.envelope import Envelope, Issue, Project, SdkDisclosure, SdkInfo, emit
|
|
108
|
+
from tan.exit_codes import ExitCode
|
|
109
|
+
from tan.output_format import FORMAT_HELP, OutputFormat, resolve_format
|
|
110
|
+
|
|
111
|
+
#: Size tools probed on PATH, most-specific first. The Zephyr SDK ships
|
|
112
|
+
#: `arm-zephyr-eabi-size`; an LLVM toolchain ships `llvm-size`; host binutils
|
|
113
|
+
#: ships `size`. All speak the same Berkeley columns.
|
|
114
|
+
SIZE_TOOLS = ("arm-zephyr-eabi-size", "llvm-size", "size")
|
|
115
|
+
|
|
116
|
+
#: Seconds before the size tool is abandoned and the next measurement rung tried.
|
|
117
|
+
#: The oracle spawns it unbounded; a hung tool there hangs the whole command, and
|
|
118
|
+
#: every subprocess in this port is bounded.
|
|
119
|
+
_SIZE_TOOL_TIMEOUT = 30
|
|
120
|
+
|
|
121
|
+
#: Streaming read size for the ELF, so a large image is not slurped whole just
|
|
122
|
+
#: to read its section headers. Section headers sit at the END of an ELF, so the
|
|
123
|
+
#: file does have to be read -- but bounded, and refused above this.
|
|
124
|
+
_MAX_ELF_BYTES = 512 * 1024 * 1024
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _find_on_path(command: str) -> str | None:
|
|
128
|
+
"""Where `command` resolves on PATH -- the ABSOLUTE path, or `None`.
|
|
129
|
+
|
|
130
|
+
Port of `util::command_on_path`, hand-rolled rather than `shutil.which`,
|
|
131
|
+
which on Windows ALWAYS inserts `os.curdir` ahead of the search list (even
|
|
132
|
+
when an explicit `path=` is given). `where.exe` has the same behaviour, and
|
|
133
|
+
the oracle walks PATH by hand for exactly this reason: a project checked out
|
|
134
|
+
with its own `size.exe` at its root would otherwise be reported as
|
|
135
|
+
"available" and then SPAWNED. A project directory must never supply the
|
|
136
|
+
executable.
|
|
137
|
+
|
|
138
|
+
**tan-cli#567: it now returns the PATH, not a bool.** The walk above was
|
|
139
|
+
correct and its result was then thrown away -- `_sizes_from_size_tool` was
|
|
140
|
+
handed the bare identity and `subprocess.run` gave it straight back to
|
|
141
|
+
`CreateProcess`, whose documented search order puts the parent process's
|
|
142
|
+
current directory AHEAD of `%PATH%`. The hardened check found the real
|
|
143
|
+
`size.exe`; the spawn then ran the project's. Returning the resolved path
|
|
144
|
+
is what makes the check and the spawn agree, and it is the same
|
|
145
|
+
one-resolver rule tan-cli#510 established for the build spawn -- which is
|
|
146
|
+
why the walk itself is no longer written out here but delegated to
|
|
147
|
+
`tan.core.tool_lookup.resolve_tool`, the ONE copy (tan-cli#532).
|
|
148
|
+
|
|
149
|
+
`os.environ`, deliberately: unlike a build slice, `tan size` pins no `PATH`
|
|
150
|
+
of its own, so the environment this resolves against IS the one
|
|
151
|
+
`_sizes_from_size_tool`'s child inherits.
|
|
152
|
+
"""
|
|
153
|
+
return resolve_tool(command, os.environ).resolved
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _sizes_from_size_tool(size_bin: str, elf: str) -> tuple[int, int] | None:
|
|
157
|
+
"""Run the size tool on `elf` and parse its Berkeley output. `None` when the
|
|
158
|
+
tool fails to spawn, times out, exits non-zero, or its output does not parse
|
|
159
|
+
-- every one of which falls through to the next measurement rung rather than
|
|
160
|
+
failing the command.
|
|
161
|
+
|
|
162
|
+
`size_bin` is the ABSOLUTE path [`_find_on_path`] resolved, never a bare
|
|
163
|
+
identity (tan-cli#567). A bare name here would be handed back to the
|
|
164
|
+
platform's own resolver, which on Windows searches the parent process's
|
|
165
|
+
current directory before `%PATH%` -- so a bare `size_bin` is refused
|
|
166
|
+
outright rather than spawned. Refused, not "resolved here": a second
|
|
167
|
+
resolution at the spawn is exactly the drift tan-cli#510 was filed over,
|
|
168
|
+
and this rung failing closed simply falls through to the ELF-section-header
|
|
169
|
+
measurement, which needs no subprocess at all."""
|
|
170
|
+
if not Path(size_bin).is_absolute():
|
|
171
|
+
return None
|
|
172
|
+
try:
|
|
173
|
+
out = subprocess.run(
|
|
174
|
+
[size_bin, elf],
|
|
175
|
+
capture_output=True,
|
|
176
|
+
text=True,
|
|
177
|
+
encoding="utf-8",
|
|
178
|
+
errors="replace",
|
|
179
|
+
timeout=_SIZE_TOOL_TIMEOUT,
|
|
180
|
+
env=spawn_env(),
|
|
181
|
+
check=False,
|
|
182
|
+
)
|
|
183
|
+
except (OSError, subprocess.SubprocessError, ValueError):
|
|
184
|
+
return None
|
|
185
|
+
if out.returncode != 0:
|
|
186
|
+
return None
|
|
187
|
+
return parse_berkeley_size(out.stdout or "")
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _read_bytes(path: str) -> bytes | None:
|
|
191
|
+
try:
|
|
192
|
+
if os.stat(path).st_size > _MAX_ELF_BYTES:
|
|
193
|
+
return None
|
|
194
|
+
with open(path, "rb") as handle:
|
|
195
|
+
return handle.read()
|
|
196
|
+
except OSError:
|
|
197
|
+
return None
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _read_text(path: str) -> str | None:
|
|
201
|
+
try:
|
|
202
|
+
with open(path, encoding="utf-8", newline="") as handle:
|
|
203
|
+
return handle.read()
|
|
204
|
+
except (OSError, UnicodeDecodeError, ValueError):
|
|
205
|
+
return None
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _minus_off_budget_ram(
|
|
209
|
+
sizes: tuple[int, int], elf: str, spans: tuple[tuple[int, int], ...]
|
|
210
|
+
) -> tuple[int, int]:
|
|
211
|
+
"""`sizes` with the RAM that the ELF places inside *spans* removed; unchanged
|
|
212
|
+
when there are no spans or the section headers cannot be read."""
|
|
213
|
+
if not spans:
|
|
214
|
+
return sizes
|
|
215
|
+
raw = _read_bytes(elf)
|
|
216
|
+
if raw is None:
|
|
217
|
+
return sizes
|
|
218
|
+
whole = sizes_from_elf_sections(raw)
|
|
219
|
+
kept = sizes_from_elf_sections(raw, spans)
|
|
220
|
+
if whole is None or kept is None:
|
|
221
|
+
return sizes
|
|
222
|
+
return sizes[0], max(sizes[1] - (whole[1] - kept[1]), 0)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _extract_sizes(
|
|
226
|
+
elf_candidates: list[str],
|
|
227
|
+
footprint_dirs: list[str],
|
|
228
|
+
size_bin: str | None,
|
|
229
|
+
off_budget_spans: tuple[tuple[int, int], ...] = (),
|
|
230
|
+
) -> tuple[tuple[int, int] | None, str | None, str]:
|
|
231
|
+
"""`(sizes, source_label, probed_elf)` via the best available source, in the
|
|
232
|
+
oracle's order: the size tool (only if the elf exists), then the elf's own
|
|
233
|
+
section headers, then `rom.json` + `ram.json`.
|
|
234
|
+
|
|
235
|
+
*off_budget_spans* (tan-cli#1402): writable sections linked inside one (SRAM0
|
|
236
|
+
under an ITCM link) are not counted as RAM. The size tool cannot see section
|
|
237
|
+
addresses, so its RAM column is corrected by the section reader's difference.
|
|
238
|
+
|
|
239
|
+
`probed_elf` is the path named in a `not-built` note -- the FIRST candidate,
|
|
240
|
+
which is the one the oracle would have named.
|
|
241
|
+
"""
|
|
242
|
+
for elf in elf_candidates:
|
|
243
|
+
if not _is_file(elf):
|
|
244
|
+
continue
|
|
245
|
+
if size_bin is not None:
|
|
246
|
+
sizes = _sizes_from_size_tool(size_bin, elf)
|
|
247
|
+
if sizes is not None:
|
|
248
|
+
sizes = _minus_off_budget_ram(sizes, elf, off_budget_spans)
|
|
249
|
+
return sizes, "size-tool", elf_candidates[0]
|
|
250
|
+
# Middle rung: no size tool, or it failed to parse -- read the section
|
|
251
|
+
# headers directly so a present elf is still measured. The `pyelftools`
|
|
252
|
+
# label is kept for JSON parity with the retired command.
|
|
253
|
+
raw = _read_bytes(elf)
|
|
254
|
+
if raw is not None:
|
|
255
|
+
sizes = sizes_from_elf_sections(raw, off_budget_spans)
|
|
256
|
+
if sizes is not None:
|
|
257
|
+
return sizes, "pyelftools", elf_candidates[0]
|
|
258
|
+
for directory in footprint_dirs:
|
|
259
|
+
rom_text = _read_text(os.path.join(directory, "rom.json"))
|
|
260
|
+
ram_text = _read_text(os.path.join(directory, "ram.json"))
|
|
261
|
+
rom = None if rom_text is None else footprint_total(rom_text)
|
|
262
|
+
ram = None if ram_text is None else footprint_total(ram_text)
|
|
263
|
+
if rom is not None and ram is not None:
|
|
264
|
+
return (rom, ram), "rom/ram.json", elf_candidates[0]
|
|
265
|
+
return None, None, elf_candidates[0]
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def _read_som_preset(
|
|
269
|
+
path: str, *, metadata_root: str | None = None, warnings: list[str] | None = None,
|
|
270
|
+
skipped: list[str] | None = None,
|
|
271
|
+
) -> tuple[str, str | None] | None:
|
|
272
|
+
"""`(silicon, silicon_variant)` from a SoM preset, or `None` when it cannot
|
|
273
|
+
be used.
|
|
274
|
+
|
|
275
|
+
Only the two fields the budget needs, plus the SAME `schema_version == 2`
|
|
276
|
+
guard `parse_som_preset` applies -- a preset the oracle refuses must be
|
|
277
|
+
`unreadable SoM preset for <sku>` here too, not silently half-read. `TBD` is
|
|
278
|
+
dropped to absent exactly as `str_clean` does, which is what variant
|
|
279
|
+
resolution wants.
|
|
280
|
+
|
|
281
|
+
*metadata_root*/*warnings* (tan-cli#964): when both are given, `root` --
|
|
282
|
+
the raw parsed document, already the shape `som-preset-v2.schema.json`
|
|
283
|
+
describes, unlike `presets_cmd.parse_som_preset`'s reshaped return value
|
|
284
|
+
-- is checked against it and every violation is appended to *warnings*.
|
|
285
|
+
`tan size`'s existing degrade-to-`unknown-budget` behaviour on a bad
|
|
286
|
+
preset is UNCHANGED by this (this function still returns exactly what it
|
|
287
|
+
always returned); the schema check only adds visibility into WHY a
|
|
288
|
+
budget came back unresolved, via the envelope's `issues[]`, never a new
|
|
289
|
+
way for this function to fail.
|
|
290
|
+
"""
|
|
291
|
+
text = _read_text(path)
|
|
292
|
+
if text is None:
|
|
293
|
+
return None
|
|
294
|
+
try:
|
|
295
|
+
root = load_yaml_document(text)
|
|
296
|
+
except Exception: # noqa: BLE001 -- SystemManifestError or any PyYAML failure
|
|
297
|
+
return None
|
|
298
|
+
if not isinstance(root, dict):
|
|
299
|
+
return None
|
|
300
|
+
if not is_supported_som_schema_version(root.get("schema_version")):
|
|
301
|
+
return None
|
|
302
|
+
if metadata_root is not None:
|
|
303
|
+
schema_path = som_preset_schema_path(metadata_root)
|
|
304
|
+
if skipped is not None:
|
|
305
|
+
note = missing_schema_note(schema_path, source=path)
|
|
306
|
+
if note is not None:
|
|
307
|
+
skipped.append(note)
|
|
308
|
+
if warnings is not None:
|
|
309
|
+
warnings.extend(validate_document(root, schema_path, path))
|
|
310
|
+
silicon = _clean_str(root.get("silicon")) or ""
|
|
311
|
+
return silicon, _clean_str(root.get("silicon_variant"))
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def _som_schema_version_issue(metadata_root: str | None, sku: str | None) -> Issue | None:
|
|
315
|
+
"""tan-cli#1278: a `size.som-schema-version-skipped` warning when *sku*'s
|
|
316
|
+
preset parses but declares a `schema_version` this tan does not read.
|
|
317
|
+
|
|
318
|
+
`_read_som_preset` refuses it, and the row says only `unreadable SoM
|
|
319
|
+
preset for <sku>` -- the oracle's own words, kept for byte parity -- which
|
|
320
|
+
against a pre-v2 SDK is every SKU and names the wrong cause. This adds the
|
|
321
|
+
real one to `issues[]` without touching the row."""
|
|
322
|
+
if metadata_root is None or sku is None:
|
|
323
|
+
return None
|
|
324
|
+
path = os.path.join(metadata_root, "e1m_modules", f"{sku}.yaml")
|
|
325
|
+
text = _read_text(path)
|
|
326
|
+
if text is None:
|
|
327
|
+
return None
|
|
328
|
+
try:
|
|
329
|
+
root = load_yaml_document(text)
|
|
330
|
+
except Exception: # noqa: BLE001 -- unparseable is `unreadable`, not this
|
|
331
|
+
return None
|
|
332
|
+
if not isinstance(root, dict):
|
|
333
|
+
return None
|
|
334
|
+
version = root.get("schema_version")
|
|
335
|
+
if is_supported_som_schema_version(version):
|
|
336
|
+
return None
|
|
337
|
+
# Forward slashes, the path as the SDK root was resolved -- the same form
|
|
338
|
+
# `presets.som-schema-version-skipped` names its directory in.
|
|
339
|
+
shown = path.replace("\\", "/")
|
|
340
|
+
subject = f"the SoM preset for {sku} ({shown}) was not read, so its FLASH/RAM budget is unknown"
|
|
341
|
+
return Issue(
|
|
342
|
+
"size.som-schema-version-skipped", "warning", skipped_presets_message(subject, [version])
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def _read_som_memory_map(path: str) -> list[dict]:
|
|
347
|
+
"""The SoM preset's `memory_map:` entries, or `[]` when it declares none
|
|
348
|
+
(tan-cli#747).
|
|
349
|
+
|
|
350
|
+
Read here rather than widened into `read_sdk_som_and_soc`'s return tuple:
|
|
351
|
+
that walk is shared with `tan debug-config`, and growing a tuple every
|
|
352
|
+
caller destructures to give ONE of them a field it alone needs is how the
|
|
353
|
+
two readers drift apart -- the thing that walk exists to prevent. The
|
|
354
|
+
preset is small and already on this path, so re-reading it is cheaper than
|
|
355
|
+
a shared shape nobody else wants.
|
|
356
|
+
|
|
357
|
+
Deliberately NOT `schema_version`-guarded a second time: the caller has
|
|
358
|
+
already been through `_read_som_preset`/`read_sdk_som_and_soc` for this
|
|
359
|
+
same file, so a preset that fails that guard never reaches here. A
|
|
360
|
+
non-list `memory_map`, or entries that are not mappings, degrade to `[]`
|
|
361
|
+
/are skipped downstream -- an unreadable budget must fall back to
|
|
362
|
+
`mram_mb`, never raise out of `tan size`.
|
|
363
|
+
"""
|
|
364
|
+
text = _read_text(path)
|
|
365
|
+
if text is None:
|
|
366
|
+
return []
|
|
367
|
+
try:
|
|
368
|
+
root = load_yaml_document(text)
|
|
369
|
+
except Exception: # noqa: BLE001 -- SystemManifestError or any PyYAML failure
|
|
370
|
+
return []
|
|
371
|
+
if not isinstance(root, dict):
|
|
372
|
+
return []
|
|
373
|
+
entries = root.get("memory_map")
|
|
374
|
+
if not isinstance(entries, list):
|
|
375
|
+
return []
|
|
376
|
+
return [e for e in entries if isinstance(e, dict)]
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def _clean_str(value: Any) -> str | None:
|
|
380
|
+
"""`str_clean`: a string, with the `TBD` sentinel dropped to absent.
|
|
381
|
+
|
|
382
|
+
Delegates to `tan.core.pending.is_pending_placeholder` (#276) for the
|
|
383
|
+
trimmed, non-case-folded, non-substring comparison shared with `tan
|
|
384
|
+
flash` -- not a second hand-rolled `== "TBD"`."""
|
|
385
|
+
if not isinstance(value, str) or is_pending_placeholder(value):
|
|
386
|
+
return None
|
|
387
|
+
return value
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def _as_f64(value: Any) -> float | None:
|
|
391
|
+
"""serde_json's `as_f64`: a JSON number, never a bool and never a string.
|
|
392
|
+
|
|
393
|
+
The `float()` is guarded (tan-cli#499 defect 8, REVIEW round). Python's
|
|
394
|
+
`json` reads an arbitrarily long integer literal exactly, so a 400-digit
|
|
395
|
+
`mram_mb` / `soc_flash_mb` / `tcm_kb` in a SoC JSON arrives here as an
|
|
396
|
+
`int` that `float()` cannot represent -- `OverflowError: int too large to
|
|
397
|
+
convert to float`, which escaped to `size`'s catch-all and collapsed the
|
|
398
|
+
WHOLE run: measured `exit 5 size.internal-failure`, `data.slices` empty
|
|
399
|
+
(every other slice's measurement discarded). The first pass at defect 8
|
|
400
|
+
fixed `core/size.py`'s three casts and left this one, so the same input
|
|
401
|
+
class still reached the same exit-5 point through a different door.
|
|
402
|
+
|
|
403
|
+
`None` -- "no such number" -- is the answer, which is what a value serde_json
|
|
404
|
+
could not have produced deserves; `resolve_budget` then leaves that region
|
|
405
|
+
unresolved instead of guessing. Measured after: the same SoC JSON is
|
|
406
|
+
`exit 0`, other slices intact. Still not byte-identical to the oracle, which
|
|
407
|
+
refuses the whole document and answers `budget_note: "unreadable SoM preset
|
|
408
|
+
for <SKU>"` -- that residual lives in `_read_soc`'s degrade-to-empty (and in
|
|
409
|
+
`json.loads` accepting literals serde_json rejects), not here.
|
|
410
|
+
"""
|
|
411
|
+
if isinstance(value, bool) or not isinstance(value, (int, float)):
|
|
412
|
+
return None
|
|
413
|
+
try:
|
|
414
|
+
return float(value)
|
|
415
|
+
except OverflowError:
|
|
416
|
+
return None
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def _read_soc(
|
|
420
|
+
path: str, *, metadata_root: str | None = None, warnings: list[str] | None = None,
|
|
421
|
+
skipped: list[str] | None = None,
|
|
422
|
+
) -> tuple[list[dict], float | None, list[tuple[str, float | None]]]:
|
|
423
|
+
"""`(variants, soc_flash_mb, soc_cores)` from a SoC JSON, defaulting to
|
|
424
|
+
empty on any failure.
|
|
425
|
+
|
|
426
|
+
`variants` is all-or-nothing on purpose: the oracle deserializes the whole
|
|
427
|
+
list with `unwrap_or_default()`, so ONE malformed entry empties it. Reading
|
|
428
|
+
the good ones here would resolve a budget the shipped binary does not.
|
|
429
|
+
|
|
430
|
+
*metadata_root*/*warnings* (tan-cli#964): see `_read_som_preset` -- same
|
|
431
|
+
contract, `soc-spec-v1.schema.json` instead. This is what closes the
|
|
432
|
+
ORIGINAL tan-cli#957 crash class for `tan size`: `soc_cores` below already
|
|
433
|
+
guards `core.get("tcm_kb")` through `_as_f64` and `core_id` through
|
|
434
|
+
`isinstance(core_id, str)`, so a schema-invalid `cores[].type` (the
|
|
435
|
+
field THIS reader never even looks at) could previously resolve a budget
|
|
436
|
+
from a document nothing had ever validated, with no trace on the wire.
|
|
437
|
+
"""
|
|
438
|
+
text = _read_text(path)
|
|
439
|
+
if text is None:
|
|
440
|
+
return [], None, []
|
|
441
|
+
try:
|
|
442
|
+
soc = json.loads(text)
|
|
443
|
+
except (ValueError, RecursionError):
|
|
444
|
+
return [], None, []
|
|
445
|
+
if not isinstance(soc, dict):
|
|
446
|
+
return [], None, []
|
|
447
|
+
if metadata_root is not None:
|
|
448
|
+
schema_path = soc_spec_schema_path(metadata_root)
|
|
449
|
+
if skipped is not None:
|
|
450
|
+
note = missing_schema_note(schema_path, source=path)
|
|
451
|
+
if note is not None:
|
|
452
|
+
skipped.append(note)
|
|
453
|
+
if warnings is not None:
|
|
454
|
+
warnings.extend(validate_document(soc, schema_path, path))
|
|
455
|
+
|
|
456
|
+
variants: list[dict] = []
|
|
457
|
+
raw_variants = soc.get("variants")
|
|
458
|
+
if raw_variants is not None:
|
|
459
|
+
variants = _coerce_variants(raw_variants)
|
|
460
|
+
|
|
461
|
+
soc_flash_mb = _as_f64(soc.get("soc_flash_mb"))
|
|
462
|
+
|
|
463
|
+
soc_cores: list[tuple[str, float | None]] = []
|
|
464
|
+
raw_cores = soc.get("cores")
|
|
465
|
+
if isinstance(raw_cores, list):
|
|
466
|
+
for core in raw_cores:
|
|
467
|
+
if not isinstance(core, dict):
|
|
468
|
+
continue
|
|
469
|
+
core_id = core.get("id")
|
|
470
|
+
if not isinstance(core_id, str):
|
|
471
|
+
continue # `filter_map` skips an entry with no string `id`
|
|
472
|
+
soc_cores.append((core_id, _as_f64(core.get("tcm_kb"))))
|
|
473
|
+
return variants, soc_flash_mb, soc_cores
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
def _coerce_variants(raw: Any) -> list[dict]:
|
|
477
|
+
if not isinstance(raw, list):
|
|
478
|
+
return []
|
|
479
|
+
out: list[dict] = []
|
|
480
|
+
for entry in raw:
|
|
481
|
+
if not isinstance(entry, dict):
|
|
482
|
+
return []
|
|
483
|
+
order_code = entry.get("order_code")
|
|
484
|
+
if order_code is not None and not isinstance(order_code, str):
|
|
485
|
+
return []
|
|
486
|
+
skus = entry.get("alp_module_skus", [])
|
|
487
|
+
if not isinstance(skus, list) or any(not isinstance(s, str) for s in skus):
|
|
488
|
+
return []
|
|
489
|
+
mram = entry.get("mram_mb")
|
|
490
|
+
if mram is not None and (isinstance(mram, bool) or not isinstance(mram, (int, float))):
|
|
491
|
+
return []
|
|
492
|
+
banks = entry.get("sram_banks_kb", {})
|
|
493
|
+
if not isinstance(banks, dict):
|
|
494
|
+
return []
|
|
495
|
+
out.append(entry)
|
|
496
|
+
return out
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
def _resolve_slice_budget(
|
|
500
|
+
sku: str | None, core_id: str, metadata_root: str | None,
|
|
501
|
+
*, warnings: list[str] | None = None, skipped: list[str] | None = None,
|
|
502
|
+
) -> MemoryBudget:
|
|
503
|
+
"""One core's `(flash_total, ram_total)` from the SoM preset + SoC JSON under
|
|
504
|
+
`<sdk>/metadata`. The pure FLASH/RAM/note logic is `core.size.resolve_budget`;
|
|
505
|
+
this only reads the files.
|
|
506
|
+
|
|
507
|
+
*warnings* (tan-cli#964) is threaded to `read_sdk_som_and_soc` unchanged;
|
|
508
|
+
see its own docstring. *skipped* (tan-cli#964 review, major 6) is the
|
|
509
|
+
same threading for the "skip-but-disclose" collector."""
|
|
510
|
+
if sku is None:
|
|
511
|
+
return budget_note_only("no SKU in manifest")
|
|
512
|
+
if metadata_root is None:
|
|
513
|
+
return budget_note_only(f"no SoM preset for {sku}")
|
|
514
|
+
|
|
515
|
+
preset_path = os.path.join(metadata_root, "e1m_modules", f"{sku}.yaml")
|
|
516
|
+
if not _is_file(preset_path):
|
|
517
|
+
return budget_note_only(f"no SoM preset for {sku}")
|
|
518
|
+
walked = read_sdk_som_and_soc(metadata_root, sku, warnings=warnings, skipped=skipped)
|
|
519
|
+
if walked is None:
|
|
520
|
+
return budget_note_only(f"unreadable SoM preset for {sku}")
|
|
521
|
+
_silicon, silicon_variant, variants, soc_flash_mb, soc_cores = walked
|
|
522
|
+
|
|
523
|
+
variant = resolve_variant(silicon_variant, sku, variants)
|
|
524
|
+
mram_mb = None if variant is None else _as_f64(variant.get("mram_mb"))
|
|
525
|
+
banks = [] if variant is None else sram_banks(variant)
|
|
526
|
+
# tan-cli#747: this core's OWN slot0 window, when the SoM declares one, is
|
|
527
|
+
# the only FLASH a core can link into -- `mram_mb` is the whole part,
|
|
528
|
+
# including the other core's slot0.
|
|
529
|
+
memory_map = _read_som_memory_map(preset_path)
|
|
530
|
+
return resolve_budget(
|
|
531
|
+
core_id, mram_mb, soc_flash_mb, banks, soc_cores, memory_map
|
|
532
|
+
)
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
def _measure_slice(
|
|
536
|
+
slice_: dict,
|
|
537
|
+
build_root: str,
|
|
538
|
+
sku: str | None,
|
|
539
|
+
metadata_root: str | None,
|
|
540
|
+
size_bin: str | None,
|
|
541
|
+
*,
|
|
542
|
+
warnings: list[str] | None = None,
|
|
543
|
+
skipped: list[str] | None = None,
|
|
544
|
+
) -> SliceSize:
|
|
545
|
+
"""Measure + budget one manifest slice into a `SliceSize` row.
|
|
546
|
+
|
|
547
|
+
*warnings*/*skipped* (tan-cli#964 / review major 6) are threaded to
|
|
548
|
+
`_resolve_slice_budget` unchanged."""
|
|
549
|
+
core_id = str(slice_.get("core_id"))
|
|
550
|
+
os_name = str(slice_.get("os"))
|
|
551
|
+
|
|
552
|
+
if os_name != "zephyr":
|
|
553
|
+
return SliceSize(
|
|
554
|
+
core_id=core_id,
|
|
555
|
+
os=os_name,
|
|
556
|
+
status="n/a",
|
|
557
|
+
note="no Zephyr image (Yocto/baremetal)",
|
|
558
|
+
)
|
|
559
|
+
|
|
560
|
+
budget = _resolve_slice_budget(
|
|
561
|
+
sku, core_id, metadata_root, warnings=warnings, skipped=skipped
|
|
562
|
+
)
|
|
563
|
+
sizes, source, probed_elf = _extract_sizes(
|
|
564
|
+
slice_elf_candidates(slice_, build_root),
|
|
565
|
+
slice_footprint_dirs(slice_, build_root),
|
|
566
|
+
size_bin,
|
|
567
|
+
budget.ram_off_budget_spans,
|
|
568
|
+
)
|
|
569
|
+
|
|
570
|
+
# tan-cli#499 defect 2: `size` never consulted the manifest `status`, so a
|
|
571
|
+
# slice recorded `failed`/`skipped` was still measured from whatever
|
|
572
|
+
# `zephyr.elf` was left on disk -- RUN 1's artefact -- and emitted as an
|
|
573
|
+
# ordinary measured row (`source: "size-tool"`, exit 0, `issues: []`).
|
|
574
|
+
# `overlay_run_results_raw` deliberately PRESERVES the previous
|
|
575
|
+
# `output_artefact` when a run carries none, and an empty one falls
|
|
576
|
+
# through to the preserved `build_dir` probe, so the compensation
|
|
577
|
+
# `build_cmd` applies on the WRITE side does not help. Worse under
|
|
578
|
+
# `--fail-over-budget`, which then computes its verdict from stale bytes.
|
|
579
|
+
# This is the same stale-artefact class `flash_plan.slice_should_flash`
|
|
580
|
+
# and `image_bundle.slice_should_bundle` both guard -- whose own docstring
|
|
581
|
+
# claims the predicate is "shared on purpose so `flash` and `image` can
|
|
582
|
+
# never disagree about which artefacts are real"; `size` consulted
|
|
583
|
+
# neither.
|
|
584
|
+
#
|
|
585
|
+
# Only a DECLARED, non-ok status refuses. An ABSENT `status` still
|
|
586
|
+
# measures, unchanged: a manifest that omits the key says nothing about
|
|
587
|
+
# whether the slice built, and refusing there would break the oracle
|
|
588
|
+
# parity every no-status case has today (the whole `test_field_type_
|
|
589
|
+
# leniency_parity` family, and `test_size_measured_slice_and_the_sdk_
|
|
590
|
+
# envelope_key`, all write slices with no `status`).
|
|
591
|
+
#
|
|
592
|
+
# And only when a measurement was actually SUPPRESSED. When the probe
|
|
593
|
+
# found nothing anyway the row is already `not-built` for the honest
|
|
594
|
+
# reason, and saying so in the oracle's own words keeps byte parity on
|
|
595
|
+
# every frozen case that carries a non-ok status with no artefact behind
|
|
596
|
+
# it -- so this divergence materialises exactly where the defect does.
|
|
597
|
+
declared_status = slice_.get("status")
|
|
598
|
+
stale = (
|
|
599
|
+
sizes is not None
|
|
600
|
+
and isinstance(declared_status, str)
|
|
601
|
+
and not slice_should_flash(declared_status)
|
|
602
|
+
)
|
|
603
|
+
if stale:
|
|
604
|
+
return SliceSize(
|
|
605
|
+
core_id=core_id,
|
|
606
|
+
os=os_name,
|
|
607
|
+
status="not-built",
|
|
608
|
+
flash_total=budget.flash_total,
|
|
609
|
+
ram_total=budget.ram_total,
|
|
610
|
+
note=budget.note,
|
|
611
|
+
notes=[
|
|
612
|
+
f"slice did not build (manifest status: {declared_status}); "
|
|
613
|
+
f"refusing to report a stale measurement from {probed_elf}"
|
|
614
|
+
],
|
|
615
|
+
)
|
|
616
|
+
|
|
617
|
+
if sizes is None:
|
|
618
|
+
return SliceSize(
|
|
619
|
+
core_id=core_id,
|
|
620
|
+
os=os_name,
|
|
621
|
+
status="not-built",
|
|
622
|
+
flash_total=budget.flash_total,
|
|
623
|
+
ram_total=budget.ram_total,
|
|
624
|
+
note=budget.note,
|
|
625
|
+
notes=[f"no footprint source at {probed_elf}"],
|
|
626
|
+
)
|
|
627
|
+
|
|
628
|
+
flash_used, ram_used = sizes
|
|
629
|
+
if budget.flash_total is None and budget.ram_total is None:
|
|
630
|
+
status = "no-budget"
|
|
631
|
+
else:
|
|
632
|
+
status = classify(
|
|
633
|
+
flash_used, budget.flash_total, ram_used, budget.ram_total
|
|
634
|
+
)
|
|
635
|
+
return SliceSize(
|
|
636
|
+
core_id=core_id,
|
|
637
|
+
os=os_name,
|
|
638
|
+
status=status,
|
|
639
|
+
flash_used=flash_used,
|
|
640
|
+
flash_total=budget.flash_total,
|
|
641
|
+
ram_used=ram_used,
|
|
642
|
+
ram_total=budget.ram_total,
|
|
643
|
+
source=source,
|
|
644
|
+
note=budget.note,
|
|
645
|
+
)
|
|
646
|
+
|
|
647
|
+
|
|
648
|
+
def _use_color(no_color: bool, ci: bool) -> bool:
|
|
649
|
+
"""Delegates to `tan.env.use_color` (tan-cli's UX-polish sweep moved the
|
|
650
|
+
logic there so `doctor_cmd` can reuse it without importing this command
|
|
651
|
+
module) -- kept as a same-named wrapper so this file's own call site and
|
|
652
|
+
`test_size_command.py` stay untouched."""
|
|
653
|
+
return use_color(no_color, ci)
|
|
654
|
+
|
|
655
|
+
|
|
656
|
+
def _join_core_ids(rows: list[SliceSize]) -> str:
|
|
657
|
+
return ", ".join(r.core_id for r in rows)
|
|
658
|
+
|
|
659
|
+
|
|
660
|
+
@dataclass
|
|
661
|
+
class _Outcome:
|
|
662
|
+
"""What one run produced. Built and returned, never emitted in place, so the
|
|
663
|
+
exception guard in [`size`] can wrap the whole computation without also
|
|
664
|
+
catching `typer.Exit` (a RuntimeError subclass, not a SystemExit)."""
|
|
665
|
+
|
|
666
|
+
exit_code: ExitCode
|
|
667
|
+
data: dict[str, Any]
|
|
668
|
+
project: Project
|
|
669
|
+
issues: list[Issue]
|
|
670
|
+
text: list[str]
|
|
671
|
+
#: Absent, never `null`. Carried from the project resolution that produced
|
|
672
|
+
#: `project`, never a second lookup -- so it is present on the manifest-error
|
|
673
|
+
#: paths too (`Envelope::new` reads what `resolve_cli_project_context`
|
|
674
|
+
#: recorded, and that ran before the manifest was opened).
|
|
675
|
+
sdk: SdkInfo | None = None
|
|
676
|
+
|
|
677
|
+
|
|
678
|
+
def _sdk_warning_lines(issues: list[Issue]) -> list[str]:
|
|
679
|
+
"""The text-mode rendering of every issue `_run` has collected by this
|
|
680
|
+
point -- originally just the `sdk_resolution_issues` pair (tan-cli#497
|
|
681
|
+
defect 5), now also `size.metadata-schema-invalid`/`-unchecked`
|
|
682
|
+
(tan-cli#964). `{severity}: {message}` -- the shape `tan build` and `tan
|
|
683
|
+
run` already print a resolution warning with, so the same workspace
|
|
684
|
+
reads the same way whichever command a developer runs.
|
|
685
|
+
|
|
686
|
+
tan-cli#964 review (minor 11): a schema-invalid issue's own `message`
|
|
687
|
+
(`<file>: <pointer>: <what jsonschema found>`) does not itself say
|
|
688
|
+
"schema", so this text line could not be told apart from any other
|
|
689
|
+
warning at a glance -- unlike the REFUSE path's "does not validate
|
|
690
|
+
against soc-spec-v1" wording. Tagged here, at the print site, rather
|
|
691
|
+
than folded into `message` itself, which is the same string the JSON
|
|
692
|
+
envelope carries and this PR's own tests pin verbatim.
|
|
693
|
+
"""
|
|
694
|
+
return [
|
|
695
|
+
f"{issue.severity}: "
|
|
696
|
+
f"{'schema: ' if issue.code.endswith('.metadata-schema-invalid') else ''}"
|
|
697
|
+
f"{issue.message}"
|
|
698
|
+
for issue in issues
|
|
699
|
+
]
|
|
700
|
+
|
|
701
|
+
|
|
702
|
+
def _error_outcome(
|
|
703
|
+
project: Project,
|
|
704
|
+
context: ProjectContext,
|
|
705
|
+
code: str,
|
|
706
|
+
message: str,
|
|
707
|
+
) -> _Outcome:
|
|
708
|
+
"""A hard manifest error: exit 1, an empty `alp-size/1` report as `data` so
|
|
709
|
+
the envelope shape stays stable, and the message prefixed for text mode.
|
|
710
|
+
|
|
711
|
+
tan-cli#464 review: this used to take a bare `sdk: SdkInfo | None` and
|
|
712
|
+
report only the `code`/`message` issue -- so a manifest gate that fires
|
|
713
|
+
before `_run`'s own `sdk_resolution_issues` call further down reported
|
|
714
|
+
`sdk.sourceTier: "globalDefault"` with neither
|
|
715
|
+
`sdk.project-pin-unresolved` nor `sdk.global-default-foreign-project`,
|
|
716
|
+
even when one applied. Takes the whole `context` instead of just its `sdk`
|
|
717
|
+
field so it can compute the same pair `_run` computes on the happy path,
|
|
718
|
+
from the one shared `sdk_resolution_issues` -- no future early return
|
|
719
|
+
through here can drop them again.
|
|
720
|
+
|
|
721
|
+
tan-cli#497 defect 5 (the `size` sibling the issue names alongside
|
|
722
|
+
`image`): that fix reached `issues` and stopped there -- `text` was built
|
|
723
|
+
without the pair, so the DEFAULT mode dropped both warnings while
|
|
724
|
+
`--format json` reported them. Composed from the SAME list now.
|
|
725
|
+
"""
|
|
726
|
+
issues = sdk_resolution_issues(
|
|
727
|
+
context.broken_project_pin, context.sdk_source_tier, context.foreign_global_default_for
|
|
728
|
+
)
|
|
729
|
+
text = _sdk_warning_lines(issues)
|
|
730
|
+
issues.append(Issue(code, "error", message))
|
|
731
|
+
return _Outcome(
|
|
732
|
+
ExitCode.RUNTIME_FAILURE,
|
|
733
|
+
build_size_report([]),
|
|
734
|
+
project,
|
|
735
|
+
issues,
|
|
736
|
+
[*text, f"size: {message}"],
|
|
737
|
+
context.sdk,
|
|
738
|
+
)
|
|
739
|
+
|
|
740
|
+
|
|
741
|
+
def _run(
|
|
742
|
+
*,
|
|
743
|
+
app_path: str,
|
|
744
|
+
build_root_arg: str | None,
|
|
745
|
+
board: str | None,
|
|
746
|
+
fail_over_budget: bool,
|
|
747
|
+
project_arg: str | None,
|
|
748
|
+
board_yaml_arg: str | None,
|
|
749
|
+
sdk_root_arg: str | None,
|
|
750
|
+
json_mode: bool,
|
|
751
|
+
no_color: bool,
|
|
752
|
+
ci: bool,
|
|
753
|
+
disclosure: SdkDisclosure,
|
|
754
|
+
) -> _Outcome:
|
|
755
|
+
"""`disclosure` is the caller's, by reference -- the resolution facts are
|
|
756
|
+
computed HERE and `size`'s `size.internal-failure` catch-all needs a name
|
|
757
|
+
to read them from once this function has already raised. See
|
|
758
|
+
`SdkDisclosure`."""
|
|
759
|
+
context: ProjectContext = resolve_project_context(
|
|
760
|
+
project_arg, board_yaml_arg, sdk_root_arg
|
|
761
|
+
)
|
|
762
|
+
project = context.project()
|
|
763
|
+
# Recorded the instant the ladder answers, ahead of every other step this
|
|
764
|
+
# function performs -- all of which can raise something unenumerated.
|
|
765
|
+
disclosure.record(
|
|
766
|
+
context.sdk,
|
|
767
|
+
sdk_resolution_issues(
|
|
768
|
+
context.broken_project_pin,
|
|
769
|
+
context.sdk_source_tier,
|
|
770
|
+
context.foreign_global_default_for,
|
|
771
|
+
),
|
|
772
|
+
)
|
|
773
|
+
|
|
774
|
+
app_base = resolve_app_base(app_path, context.workspace_root)
|
|
775
|
+
build_root = resolve_build_root(build_root_arg, app_base)
|
|
776
|
+
metadata_sdk = resolve_metadata_sdk_root(sdk_root_arg, context.workspace_root)
|
|
777
|
+
if metadata_sdk is None and (sdk_root_arg or "").strip():
|
|
778
|
+
# tan-cli#1463: an EXPLICIT `--sdk-root` that did not resolve used to
|
|
779
|
+
# continue silently with `metadata_sdk=None` and size the build with
|
|
780
|
+
# no SDK metadata. No flag + no checkout still continues (nothing was
|
|
781
|
+
# asked for). Intentional divergence from the v0.4.1 oracle, which
|
|
782
|
+
# reached `size.manifest-unavailable`; this refusal now precedes the
|
|
783
|
+
# manifest checks.
|
|
784
|
+
return _error_outcome(
|
|
785
|
+
project,
|
|
786
|
+
context,
|
|
787
|
+
"size.sdk-root-unresolved",
|
|
788
|
+
rejected_sdk_root_message(sdk_root_arg, "No size report was produced."),
|
|
789
|
+
)
|
|
790
|
+
metadata_root = (
|
|
791
|
+
None if metadata_sdk is None else os.path.join(str(metadata_sdk), "metadata")
|
|
792
|
+
)
|
|
793
|
+
|
|
794
|
+
build_root = effective_build_root(build_root)
|
|
795
|
+
try:
|
|
796
|
+
_text, manifest = load_manifest(build_root)
|
|
797
|
+
except ManifestUnavailable as err:
|
|
798
|
+
return _error_outcome(
|
|
799
|
+
project,
|
|
800
|
+
context,
|
|
801
|
+
"size.manifest-unavailable",
|
|
802
|
+
f"no system-manifest.yaml at {err.path}; run `tan build` first "
|
|
803
|
+
f"({err.detail}).",
|
|
804
|
+
)
|
|
805
|
+
except ManifestInvalid as err:
|
|
806
|
+
return _error_outcome(
|
|
807
|
+
project, context, "size.manifest-invalid", f"{err.path}: {err.detail}"
|
|
808
|
+
)
|
|
809
|
+
|
|
810
|
+
sku = board if board is not None else (manifest.sku.strip() or None)
|
|
811
|
+
# tan-cli#567: the RESOLVED absolute path of the first size tool that is
|
|
812
|
+
# really on PATH, not its bare name -- that path is what
|
|
813
|
+
# `_sizes_from_size_tool` spawns, so the current directory cannot supply a
|
|
814
|
+
# different `size.exe` than the one this check just approved. `None` still
|
|
815
|
+
# means "no size tool on PATH", and the fallback rungs below are unchanged.
|
|
816
|
+
size_bin = next(filter(None, (_find_on_path(t) for t in SIZE_TOOLS)), None)
|
|
817
|
+
# tan-cli#964: one collector shared across every slice -- several slices
|
|
818
|
+
# on a heterogeneous SoM resolve their budget off the SAME SoM
|
|
819
|
+
# preset/SoC JSON, and `dict.fromkeys` below dedupes the repeats rather
|
|
820
|
+
# than reporting one violation once per core.
|
|
821
|
+
schema_warnings: list[str] = []
|
|
822
|
+
schema_skipped: list[str] = []
|
|
823
|
+
rows = [
|
|
824
|
+
_measure_slice(
|
|
825
|
+
s, build_root, sku, metadata_root, size_bin,
|
|
826
|
+
warnings=schema_warnings, skipped=schema_skipped,
|
|
827
|
+
)
|
|
828
|
+
for s in manifest.slices
|
|
829
|
+
]
|
|
830
|
+
schema_warnings = list(dict.fromkeys(schema_warnings))
|
|
831
|
+
schema_skipped = list(dict.fromkeys(schema_skipped))
|
|
832
|
+
|
|
833
|
+
text: list[str] = []
|
|
834
|
+
# The same pair `_error_outcome` above computes for a manifest-gate
|
|
835
|
+
# refusal, from the same shared `sdk_resolution_issues` -- read back off
|
|
836
|
+
# the disclosure rather than computed a second time, so this path, that one
|
|
837
|
+
# and the catch-all can never disagree about whether either warning applies.
|
|
838
|
+
issues: list[Issue] = list(disclosure.issues)
|
|
839
|
+
# tan-cli#964: warn-and-continue, never refuse -- `tan size` already
|
|
840
|
+
# degrades a bad SoM preset/SoC JSON to `budget: unknown` (see
|
|
841
|
+
# `_resolve_slice_budget`'s own docstring); this only makes WHY visible.
|
|
842
|
+
issues.extend(
|
|
843
|
+
Issue("size.metadata-schema-invalid", "warning", w) for w in schema_warnings
|
|
844
|
+
)
|
|
845
|
+
# tan-cli#964 review (major 6, "skip-but-disclose"): `info`, not
|
|
846
|
+
# `warning` -- nothing here says a document is wrong, only that a
|
|
847
|
+
# schema file to check it against is absent from this checkout.
|
|
848
|
+
issues.extend(
|
|
849
|
+
Issue("size.metadata-schema-unchecked", "info", w) for w in schema_skipped
|
|
850
|
+
)
|
|
851
|
+
version_issue = _som_schema_version_issue(metadata_root, sku)
|
|
852
|
+
if version_issue is not None:
|
|
853
|
+
issues.append(version_issue)
|
|
854
|
+
exit_code = ExitCode.SUCCESS
|
|
855
|
+
|
|
856
|
+
if not json_mode:
|
|
857
|
+
# tan-cli#497 defect 5, happy-path half: the pair above went into
|
|
858
|
+
# `issues` and nowhere else, so a measured report solved out of a
|
|
859
|
+
# FALLBACK checkout printed its table with no mention that the
|
|
860
|
+
# project pin had been ignored. Ahead of the table so it is not lost
|
|
861
|
+
# below a long slice list.
|
|
862
|
+
text.extend(_sdk_warning_lines(issues))
|
|
863
|
+
text.extend(render_table_lines(rows, _use_color(no_color, ci)))
|
|
864
|
+
if size_bin is None:
|
|
865
|
+
text.append(
|
|
866
|
+
"size: no size tool on PATH (arm-zephyr-eabi-size / llvm-size / "
|
|
867
|
+
"size); measured from ELF section headers (or rom.json+ram.json)."
|
|
868
|
+
)
|
|
869
|
+
|
|
870
|
+
if fail_over_budget:
|
|
871
|
+
unknown = unknown_budget_rows(rows)
|
|
872
|
+
if unknown:
|
|
873
|
+
message = (
|
|
874
|
+
f"size: budget unknown for [{_join_core_ids(unknown)}] "
|
|
875
|
+
"— skipped by --fail-over-budget (no guess)."
|
|
876
|
+
)
|
|
877
|
+
if not json_mode:
|
|
878
|
+
text.append(message)
|
|
879
|
+
issues.append(Issue("size.budget-unknown", "info", message))
|
|
880
|
+
over = over_budget_rows(rows)
|
|
881
|
+
if over:
|
|
882
|
+
message = f"size: over budget: [{_join_core_ids(over)}]."
|
|
883
|
+
if not json_mode:
|
|
884
|
+
text.append(message)
|
|
885
|
+
issues.append(Issue("size.over-budget", "error", message))
|
|
886
|
+
exit_code = ExitCode.RUNTIME_FAILURE
|
|
887
|
+
|
|
888
|
+
return _Outcome(
|
|
889
|
+
exit_code, build_size_report(rows), project, issues, text, context.sdk
|
|
890
|
+
)
|
|
891
|
+
|
|
892
|
+
|
|
893
|
+
def size(
|
|
894
|
+
ctx: typer.Context,
|
|
895
|
+
app_path: str = typer.Argument(
|
|
896
|
+
".",
|
|
897
|
+
metavar="APP_PATH",
|
|
898
|
+
help="Application source directory (default: '.'). build_root defaults "
|
|
899
|
+
"to <APP_PATH>/build. Overrides the global --project when not '.'.",
|
|
900
|
+
),
|
|
901
|
+
build_root: str = typer.Option(
|
|
902
|
+
None,
|
|
903
|
+
"--build-root",
|
|
904
|
+
metavar="PATH",
|
|
905
|
+
help="Override the build root holding system-manifest.yaml "
|
|
906
|
+
"(default: <APP_PATH>/build).",
|
|
907
|
+
),
|
|
908
|
+
board: str = typer.Option(
|
|
909
|
+
None,
|
|
910
|
+
"--board",
|
|
911
|
+
metavar="SKU",
|
|
912
|
+
help="Override the SoM SKU used to resolve the memory budget (default: "
|
|
913
|
+
"hw_info.sku from the manifest). Distinct from --board-yaml.",
|
|
914
|
+
),
|
|
915
|
+
fail_over_budget: bool = typer.Option(
|
|
916
|
+
False,
|
|
917
|
+
"--fail-over-budget",
|
|
918
|
+
help="Exit non-zero if any slice exceeds its resolved budget (slices "
|
|
919
|
+
"with an unknown budget are skipped + reported, never guessed).",
|
|
920
|
+
),
|
|
921
|
+
project: str = typer.Option(
|
|
922
|
+
None, "--project", metavar="PATH", help="Project root (defaults to '.')."
|
|
923
|
+
),
|
|
924
|
+
board_yaml: str = typer.Option(
|
|
925
|
+
None, "--board-yaml", metavar="PATH", help="Explicit board.yaml path."
|
|
926
|
+
),
|
|
927
|
+
sdk_root: str = typer.Option(
|
|
928
|
+
None, "--sdk-root", metavar="PATH", help="alp-sdk checkout root."
|
|
929
|
+
),
|
|
930
|
+
output_format: OutputFormat = typer.Option(None, "--format", help=FORMAT_HELP),
|
|
931
|
+
no_color: bool = typer.Option(False, "--no-color", help="Disable ANSI colour."),
|
|
932
|
+
ci: bool = typer.Option(False, "--ci", help="Non-interactive CI mode."),
|
|
933
|
+
) -> None:
|
|
934
|
+
"""Report per-slice firmware footprint vs the SoM memory budget."""
|
|
935
|
+
resolved_format = resolve_format(output_format, ctx.obj, choices=OutputFormat)
|
|
936
|
+
json_mode = resolved_format == "json"
|
|
937
|
+
|
|
938
|
+
# tan-cli#497 defect 5, the site the first pass missed -- see the twin
|
|
939
|
+
# comment in `image_cmd.image`. `_error_outcome` and the happy path both
|
|
940
|
+
# report the SDK-resolution pair; this handler, which runs strictly after
|
|
941
|
+
# `resolve_project_context` has already answered, reported only the crash.
|
|
942
|
+
# Recorded rather than recomputed here: the resolver is itself one of the
|
|
943
|
+
# things that can raise, and this handler must not.
|
|
944
|
+
disclosure = SdkDisclosure()
|
|
945
|
+
try:
|
|
946
|
+
outcome = _run(
|
|
947
|
+
app_path=app_path,
|
|
948
|
+
build_root_arg=build_root,
|
|
949
|
+
board=board,
|
|
950
|
+
fail_over_budget=fail_over_budget,
|
|
951
|
+
project_arg=project,
|
|
952
|
+
board_yaml_arg=board_yaml,
|
|
953
|
+
sdk_root_arg=sdk_root,
|
|
954
|
+
json_mode=json_mode,
|
|
955
|
+
no_color=no_color,
|
|
956
|
+
ci=ci,
|
|
957
|
+
disclosure=disclosure,
|
|
958
|
+
)
|
|
959
|
+
except Exception as err: # noqa: BLE001
|
|
960
|
+
# The port's most-repeated defect class: an uncaught exception escapes as
|
|
961
|
+
# a raw traceback, stdout stays EMPTY, and the extension renders nothing
|
|
962
|
+
# at all with no error on either side. Anything reaching here is a tan
|
|
963
|
+
# bug and is reported as one -- with an envelope. Nothing in this handler
|
|
964
|
+
# may itself throw: it only formats `err`, `_sdk_warning_lines` only
|
|
965
|
+
# formats, and `Project(None, None)` and `build_size_report([])` are
|
|
966
|
+
# both total.
|
|
967
|
+
outcome = _Outcome(
|
|
968
|
+
ExitCode.INTERNAL_FAILURE,
|
|
969
|
+
build_size_report([]),
|
|
970
|
+
Project(root=None, board_yaml=None),
|
|
971
|
+
[
|
|
972
|
+
*disclosure.issues,
|
|
973
|
+
Issue(
|
|
974
|
+
"size.internal-failure",
|
|
975
|
+
"error",
|
|
976
|
+
f"size failed unexpectedly: {type(err).__name__}: {err}",
|
|
977
|
+
),
|
|
978
|
+
],
|
|
979
|
+
[*_sdk_warning_lines(disclosure.issues), "size: internal failure"],
|
|
980
|
+
disclosure.sdk,
|
|
981
|
+
)
|
|
982
|
+
|
|
983
|
+
# Built ONCE, for both formats: `Envelope.__init__` appends the tan-cli#407
|
|
984
|
+
# `sdk.discovery-divergent` warning at the shared seam (`_with_sdk_
|
|
985
|
+
# divergence`), and `outcome.text` was assembled strictly before any
|
|
986
|
+
# `Envelope` existed -- so a seam-appended issue reached `--format json`
|
|
987
|
+
# and was silent on the default text channel (tan-cli#799). Diffed
|
|
988
|
+
# against `outcome.issues` (by value: `Issue` is a frozen dataclass) so
|
|
989
|
+
# only what the seam ADDED is rendered, never a duplicate of a warning
|
|
990
|
+
# `outcome.text` already carries (the pair `_run`/`_error_outcome` render
|
|
991
|
+
# via `_sdk_warning_lines` above).
|
|
992
|
+
envelope = Envelope(
|
|
993
|
+
"size",
|
|
994
|
+
outcome.project,
|
|
995
|
+
outcome.data,
|
|
996
|
+
outcome.issues,
|
|
997
|
+
outcome.exit_code,
|
|
998
|
+
sdk=outcome.sdk,
|
|
999
|
+
)
|
|
1000
|
+
if json_mode:
|
|
1001
|
+
emit(envelope)
|
|
1002
|
+
else:
|
|
1003
|
+
seam_extra = [issue for issue in envelope.issues if issue not in outcome.issues]
|
|
1004
|
+
# stdout is the envelope channel and carries nothing else, in either
|
|
1005
|
+
# mode; stderr carries no contract of its own.
|
|
1006
|
+
stream = typer.get_text_stream("stderr")
|
|
1007
|
+
for issue in seam_extra:
|
|
1008
|
+
stream.write(f"{issue.severity}: {issue.message}\n")
|
|
1009
|
+
for line in outcome.text:
|
|
1010
|
+
stream.write(f"{line}\n")
|
|
1011
|
+
raise typer.Exit(int(outcome.exit_code))
|
|
1012
|
+
|
|
1013
|
+
|
|
1014
|
+
# tan-cli#261: adds the five oracle `GlobalArgs` flags this command was still
|
|
1015
|
+
# missing (`--all`/`--non-interactive`/`--quiet`/`--target`/`--verbose`) on
|
|
1016
|
+
# top of `--no-color`/`--ci`, already declared and read above (`_use_color`);
|
|
1017
|
+
# see `tan.core.global_flags`. `ctx: typer.Context` (this command's own
|
|
1018
|
+
# `_HONOURS_ROOT_FORMAT` seam) is untouched -- appended parameters are all
|
|
1019
|
+
# keyword-only Options, never repositioned relative to it.
|
|
1020
|
+
size = accept_global_flags(size)
|