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
|
@@ -0,0 +1,2425 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""`tan debug-config` -- generate (or preview) a VS Code launch.json entry.
|
|
3
|
+
|
|
4
|
+
Port of `crates/tan-cli/src/commands/debug_config.rs`. Build a launch draft for
|
|
5
|
+
the target class + server, resolve what this project's own build already knows
|
|
6
|
+
(#66), then either preview it (`--preview`) or merge it into
|
|
7
|
+
`<workspace>/.vscode/launch.json`. A malformed existing launch.json -> exit 5;
|
|
8
|
+
a failed write -> exit 3.
|
|
9
|
+
|
|
10
|
+
A write also reads and (best-effort) rewrites one more file:
|
|
11
|
+
`<workspace>/.alp/debug-launch-provenance.json` (tan-cli#518) -- a
|
|
12
|
+
content-hash record of which `configFiles`/`setupCommands` list entries THIS
|
|
13
|
+
command itself wrote, so a LATER merge can tell its own prior output apart
|
|
14
|
+
from the customer's without guessing from position. See `tan.core.
|
|
15
|
+
launch_provenance`'s module docstring for the full design and
|
|
16
|
+
`debug_launch.create_launch_json_write_plan`'s for how it gates the merge.
|
|
17
|
+
Losing that file (deleted, corrupted, never written) never blocks a write and
|
|
18
|
+
never risks customer content -- it only makes the NEXT merge more
|
|
19
|
+
conservative.
|
|
20
|
+
|
|
21
|
+
Everything else this command refuses is the CALLER's own precondition or flag
|
|
22
|
+
value to fix, not a tan crash, so it exits `VALIDATION_FAILURE` (2)
|
|
23
|
+
(tan-cli#462, matching the distinction tan-cli#262 settled for `tan
|
|
24
|
+
validate`): a bad `--target-kind`/`--server`/target+server pairing/`--svd`/
|
|
25
|
+
`--gdbserver-address` value (tan-cli#477); a `--project` that does not exist
|
|
26
|
+
or is not a directory (tan-cli#476 half (a)); and an omitted `--target-kind`
|
|
27
|
+
this project cannot resolve to one target class -- pre-build hardware, an
|
|
28
|
+
explicit `--core` matching no slice, more than one target class with no
|
|
29
|
+
`--core` to narrow them, or no slice whose `os` maps to a target class at all.
|
|
30
|
+
Two more joined that list rather than continuing to succeed silently:
|
|
31
|
+
|
|
32
|
+
* an omitted `--target-kind` on a project offering NO signal at all -- no
|
|
33
|
+
`build/system-manifest.yaml` and no `board.yaml` `som.sku` -- which used to
|
|
34
|
+
fall through to `parse_target_kind(None)`'s `native-host` default and write
|
|
35
|
+
a `native_sim` launch configuration into whatever directory `--project`
|
|
36
|
+
named (tan-cli#476 half (b), `_target_kind_unresolved_failure`);
|
|
37
|
+
* an explicit `--core` naming a core this project's SoM does not have, on a
|
|
38
|
+
project with no build manifest to check it against, which used to leave
|
|
39
|
+
`device` as the literal `<resolved-device>` at exit 0 -- validated against
|
|
40
|
+
the SDK's own published core list instead (tan-cli#477 major 2,
|
|
41
|
+
`_sdk_published_cores`).
|
|
42
|
+
|
|
43
|
+
**The debug profile follows from the target CLASS; the customer never selects an
|
|
44
|
+
OS or a backend.** `--target-kind` names one of four classes, each of which
|
|
45
|
+
implies its adapter (cortex-debug / cppdbg / lldb), its artefact shape and its
|
|
46
|
+
legal server set. There is no `--os` and no `--backend`, and nothing here knows
|
|
47
|
+
a SKU, a device address, an I2C address or a pin name -- the hardware facts live
|
|
48
|
+
in alp-sdk `metadata/`, and the ones a launch config needs arrive through this
|
|
49
|
+
project's OWN build output (`build/system-manifest.yaml` +
|
|
50
|
+
`<build_dir>/zephyr/runners.yaml`, both written beside the build).
|
|
51
|
+
|
|
52
|
+
**Nothing here shells the SDK.** Every input is either an argument, a file
|
|
53
|
+
this project's build already wrote under the workspace, or -- since
|
|
54
|
+
alp-sdk#1026 -- a `metadata/**` file this command reads directly out of a
|
|
55
|
+
resolved SDK checkout (`_fill_debug_probe_identity_from_sdk`, below): a
|
|
56
|
+
best-effort, silent enrichment, exactly like the `board.yaml`/
|
|
57
|
+
`system-manifest.yaml` reads already were. No `alp_project.py`, no
|
|
58
|
+
`alp_orchestrate`, no subprocess of any kind ever runs, and no `sdk` envelope
|
|
59
|
+
key is emitted (tan-cli#111 follow-up -- see
|
|
60
|
+
`_resolve_project_reporting_fields`): the SDK root is resolved and read for
|
|
61
|
+
this one silent enrichment, never reported as a dependency this command
|
|
62
|
+
declares. That is the invariant the port spec records as I-32 and its
|
|
63
|
+
anti-pattern #22: giving a command an alp-sdk-checkout dependency it does not
|
|
64
|
+
declare is a silent regression that no gate catches -- READING metadata files
|
|
65
|
+
is not that; SHELLING the SDK would be.
|
|
66
|
+
|
|
67
|
+
Every failure path emits a coded envelope. A raw traceback on stdout is
|
|
68
|
+
indistinguishable, to the extension, from tan producing nothing at all -- it
|
|
69
|
+
renders an empty panel with no error -- so the outer guard in [`debug_config`]
|
|
70
|
+
converts any unexpected exception into `debug-config.internal-failure` at exit
|
|
71
|
+
5 rather than letting it escape.
|
|
72
|
+
|
|
73
|
+
**The written profile now says whether it programs the device (tan-cli#945).**
|
|
74
|
+
Two additive facts close the gap alp-sdk-vscode#586 found: a cortex-debug
|
|
75
|
+
`configuration` always carries an explicit `loadFiles` key (naming the
|
|
76
|
+
artefact it programs -- `create_launch_draft` never omits it, matching the
|
|
77
|
+
adapter's own default exactly rather than leaving it implicit), and every
|
|
78
|
+
`data` payload carries `programsDevice: bool` (`tan.core.debug_launch.
|
|
79
|
+
programs_device`), true for zephyr-mcu/baremetal-mcu, false for
|
|
80
|
+
yocto-userspace (a cppdbg attach to an already-deployed gdbserver) and
|
|
81
|
+
native-host (no target hardware exists). Neither field changes which profiles
|
|
82
|
+
this command writes to a workspace it did not author -- a hand-written
|
|
83
|
+
cortex-debug entry is still owed the same inference the extension already
|
|
84
|
+
does for it (see the issue's own "Scope, honestly").
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
from __future__ import annotations
|
|
88
|
+
|
|
89
|
+
import os
|
|
90
|
+
import stat
|
|
91
|
+
from dataclasses import dataclass
|
|
92
|
+
from pathlib import Path
|
|
93
|
+
from typing import Any
|
|
94
|
+
|
|
95
|
+
import typer
|
|
96
|
+
|
|
97
|
+
from tan.commands.build_output import read_sdk_som_and_soc, resolve_project_context
|
|
98
|
+
# The openocd-on-PATH probe is BORROWED, not rebuilt (tan-cli#1179). Both
|
|
99
|
+
# names are private to `support_bundle_cmd`, and importing them is still the
|
|
100
|
+
# right call: `_RUNTIME_EXECUTABLES[OPENOCD]` is the candidate-name table and
|
|
101
|
+
# `_first_on_path` resolves it through `doctor_cmd.on_path` -- the same single
|
|
102
|
+
# lookup `tan support-bundle` reports. A second, independently written probe
|
|
103
|
+
# here would be free to disagree with the bundle a customer attaches to the
|
|
104
|
+
# bug report about this very note, and to miss a second openocd spelling the
|
|
105
|
+
# table later learns. No cycle: nothing under `tan/commands/` imports this
|
|
106
|
+
# module, and `cli.py` already pulls `support_bundle_cmd` into every graph.
|
|
107
|
+
from tan.commands.support_bundle_cmd import _RUNTIME_EXECUTABLES, _first_on_path
|
|
108
|
+
from tan.core.atomic_write import atomic_write_text
|
|
109
|
+
from tan.core.flash_plan import FlashPlanError, fa_str_checked, validate_identifier
|
|
110
|
+
from tan.core.debug_launch import (
|
|
111
|
+
BAREMETAL_MCU,
|
|
112
|
+
GDBSERVER,
|
|
113
|
+
JLINK,
|
|
114
|
+
MANIFEST_OS_BY_TARGET,
|
|
115
|
+
NATIVE_HOST,
|
|
116
|
+
OPENOCD,
|
|
117
|
+
PYOCD,
|
|
118
|
+
SERVER_NONE,
|
|
119
|
+
TARGET_KINDS,
|
|
120
|
+
YOCTO_USERSPACE,
|
|
121
|
+
ZEPHYR_MCU,
|
|
122
|
+
DebugConfigError,
|
|
123
|
+
LaunchResolution,
|
|
124
|
+
apply_launch_resolution,
|
|
125
|
+
create_launch_draft,
|
|
126
|
+
create_launch_json_write_plan,
|
|
127
|
+
explicit_core_unknown_message,
|
|
128
|
+
multi_core_without_core_message,
|
|
129
|
+
fill_debug_probe_identity_gaps,
|
|
130
|
+
infer_target_kind,
|
|
131
|
+
is_unresolved_placeholder,
|
|
132
|
+
launch_preview_document,
|
|
133
|
+
launch_preview_notes,
|
|
134
|
+
load_files_preserved,
|
|
135
|
+
manifest_slices,
|
|
136
|
+
parse_server_kind,
|
|
137
|
+
parse_target_kind,
|
|
138
|
+
programs_device,
|
|
139
|
+
sdk_identity_overwrites,
|
|
140
|
+
sdk_identity_stranded_appends,
|
|
141
|
+
)
|
|
142
|
+
from tan.core import launch_provenance
|
|
143
|
+
from tan.core.global_flags import accept_global_flags
|
|
144
|
+
from tan.core.jsonc_splice import pretty_json
|
|
145
|
+
from tan.core.run import is_native_sim_board, native_sim_exe_beside
|
|
146
|
+
from tan.core.size import resolve_variant
|
|
147
|
+
from tan.core.timestamp import generated_at_iso
|
|
148
|
+
from tan.envelope import Envelope, Issue, Project, emit
|
|
149
|
+
from tan.exit_codes import ExitCode
|
|
150
|
+
from tan.output_format import FORMAT_HELP, OutputFormat, resolve_format
|
|
151
|
+
|
|
152
|
+
#: `data.schemaVersion` for this command's payload.
|
|
153
|
+
DATA_SCHEMA_VERSION = "1"
|
|
154
|
+
|
|
155
|
+
#: The `runners.yaml` runner id a debug server reads its arguments from.
|
|
156
|
+
#: `gdbserver`/`none` have no runner: neither is a Zephyr probe runner.
|
|
157
|
+
_RUNNER_ID = {JLINK: "jlink", OPENOCD: "openocd", PYOCD: "pyocd"}
|
|
158
|
+
|
|
159
|
+
#: `--server`'s default when this run just INFERRED `--target-kind` itself
|
|
160
|
+
#: (tan-cli#456) -- never applied over an explicit target, so the pre-existing
|
|
161
|
+
#: "no --server given" refusal there is unchanged.
|
|
162
|
+
_DEFAULT_SERVER_FOR_TARGET = {
|
|
163
|
+
ZEPHYR_MCU: JLINK,
|
|
164
|
+
BAREMETAL_MCU: JLINK,
|
|
165
|
+
YOCTO_USERSPACE: GDBSERVER,
|
|
166
|
+
NATIVE_HOST: SERVER_NONE,
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
#: The launch-configuration JSON key the SDK's debug-probe identity
|
|
170
|
+
#: (`variants[].debug`) resolves for a given server -- absent for a server the
|
|
171
|
+
#: identity has no concept of at all (`gdbserver`/`none`, neither of which
|
|
172
|
+
#: `create_launch_draft` ever pairs with a `variants[].debug` field).
|
|
173
|
+
_SERVER_IDENTITY_FIELD = {JLINK: "device", OPENOCD: "configFiles", PYOCD: "targetId"}
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
@dataclass
|
|
177
|
+
class _Outcome:
|
|
178
|
+
"""What one run produced: the envelope pieces plus the human lines. Built and
|
|
179
|
+
returned, never emitted in place, so the exception guard in [`debug_config`]
|
|
180
|
+
can wrap the whole computation without also catching `typer.Exit` (which is a
|
|
181
|
+
`RuntimeError` subclass, not a `SystemExit`, and would otherwise be swallowed
|
|
182
|
+
by a bare `except Exception`)."""
|
|
183
|
+
|
|
184
|
+
exit_code: ExitCode
|
|
185
|
+
data: dict[str, Any]
|
|
186
|
+
project: Project
|
|
187
|
+
issues: list[Issue]
|
|
188
|
+
text: list[str]
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _generated_at() -> str:
|
|
192
|
+
"""`SOURCE_DATE_EPOCH` when set, else now -- `tan.core.timestamp`, which
|
|
193
|
+
NEVER raises. That matters here specifically: this is also called from the
|
|
194
|
+
recovery path of the exception guard in `_emit_outcome`, so a throw
|
|
195
|
+
DOUBLE-FAULTS into a raw traceback with EMPTY stdout.
|
|
196
|
+
|
|
197
|
+
Millisecond precision with a `Z` suffix, matching JavaScript's
|
|
198
|
+
`toISOString()`, because the envelopes are byte-compared against the TS
|
|
199
|
+
CLI's `generatedAt` (and against the committed goldens, which spell
|
|
200
|
+
`1970-01-01T00:00:00.000Z`). A whole-second format would fail all four
|
|
201
|
+
`debug-config` fixtures.
|
|
202
|
+
"""
|
|
203
|
+
return generated_at_iso(millis=True)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _normalise(path: str) -> str:
|
|
207
|
+
"""Rust's `normalize_path(cwd.join(p))`: cwd-anchored and lexically
|
|
208
|
+
normalised, in the platform's OWN separators (this value becomes
|
|
209
|
+
`data.launchJsonPath`, which the contract harness normalises itself).
|
|
210
|
+
|
|
211
|
+
`os.path.abspath`, not `Path.resolve()`: abspath is purely lexical, so a
|
|
212
|
+
project reached through a symlink keeps the name the user typed and a path
|
|
213
|
+
that does not exist yet still resolves -- `resolve()` would rewrite both.
|
|
214
|
+
"""
|
|
215
|
+
return os.path.abspath(path)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _resolve_project_reporting_fields(
|
|
219
|
+
project_arg: str, board_yaml_arg: str | None, sdk_root_arg: str | None
|
|
220
|
+
) -> tuple[str, str, str | None, str, str | None]:
|
|
221
|
+
"""`(project.root, project.boardYaml, sdk_root, sdk_source_tier,
|
|
222
|
+
foreign_global_default_for)`. The first two, both posix, are the fields
|
|
223
|
+
this command REPORTS; `sdk_root` is read for the alp-sdk#1026 metadata
|
|
224
|
+
fallback (`_fill_debug_probe_identity_from_sdk`) below and is NEVER
|
|
225
|
+
attached to the outgoing envelope.
|
|
226
|
+
|
|
227
|
+
The last two are the SDK's PROVENANCE, and they are returned for exactly
|
|
228
|
+
one consumer: [`_sdk_core_refusal_authority`], the tan-cli#477 review
|
|
229
|
+
round. Reading a debug-probe identity out of whatever SDK happened to
|
|
230
|
+
resolve is harmless -- the worst case is a placeholder that stays a
|
|
231
|
+
placeholder. REFUSING the run at exit 2 on that same checkout's word is
|
|
232
|
+
not, so the caller has to know which tier answered before it lets the
|
|
233
|
+
answer decide anything.
|
|
234
|
+
|
|
235
|
+
Delegates to the SAME shared resolver every other command uses
|
|
236
|
+
(`build_output.resolve_project_context`, port of
|
|
237
|
+
`util::resolve_cli_project_context`) instead of a hand-rolled duplicate of
|
|
238
|
+
its workspace/board_yaml computation -- tan-cli#170: `debug-config` used to
|
|
239
|
+
hardcode `board_yaml: None` on every path, even a success with a valid
|
|
240
|
+
`board.yaml` sitting in the resolved root, while every other command took
|
|
241
|
+
both from the shared resolver.
|
|
242
|
+
|
|
243
|
+
Deliberately the `_no_sdk_report` half of the Rust resolver's contract:
|
|
244
|
+
the shared resolver's `sdk` half IS read here (unlike before alp-sdk#1026),
|
|
245
|
+
but the caller must never pass it into the `Envelope(...)` call --
|
|
246
|
+
resolving one could only add an undeclared `sdk` envelope key as a side
|
|
247
|
+
effect of a field this command otherwise merely reports (tan-cli#111
|
|
248
|
+
follow-up), and `debug-config` still drives no SDK subprocess of any kind
|
|
249
|
+
(I-32).
|
|
250
|
+
|
|
251
|
+
`board.yaml`'s existence is NOT checked HERE, matching
|
|
252
|
+
`project.rs::resolve_board_yaml_path`, which joins the configured relative
|
|
253
|
+
path onto the workspace root unconditionally -- this function only names
|
|
254
|
+
where a `board.yaml` WOULD live. [`_run`]'s caller is the seam that checks
|
|
255
|
+
(tan-cli#236, `Project.resolved`): the four hermetic goldens run in a
|
|
256
|
+
scratch directory holding no `board.yaml` at all and report a `null`
|
|
257
|
+
`project.boardYaml`, not this joined path.
|
|
258
|
+
"""
|
|
259
|
+
context = resolve_project_context(project_arg, board_yaml_arg, sdk_root_arg)
|
|
260
|
+
sdk_root = context.sdk.root if context.sdk is not None else None
|
|
261
|
+
return (
|
|
262
|
+
context.workspace_root,
|
|
263
|
+
context.board_yaml,
|
|
264
|
+
sdk_root,
|
|
265
|
+
context.sdk_source_tier,
|
|
266
|
+
context.foreign_global_default_for,
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _workspace_relative(workspace_root: str, path: str) -> str:
|
|
271
|
+
"""Rewrite a path under `workspace_root` as `${workspaceFolder}/<rel>`, so a
|
|
272
|
+
committed launch.json stays portable; an artefact outside the project (an
|
|
273
|
+
out-of-tree build root) is left absolute rather than mangled.
|
|
274
|
+
|
|
275
|
+
The tail is SLICED out of `path`'s own text, never re-rendered through
|
|
276
|
+
`Path`. Rust's `strip_prefix` hands back a borrowed subslice, so the
|
|
277
|
+
separators the manifest itself used survive; `Path.relative_to(...)` builds a
|
|
278
|
+
fresh object that re-renders with the platform separator, which on Windows
|
|
279
|
+
turns a `/`-authored manifest path into `${workspaceFolder}/build\\...`. That
|
|
280
|
+
string is handed to a debug adapter verbatim, so the difference is visible.
|
|
281
|
+
|
|
282
|
+
Rust's `strip_prefix` folds case on the Windows drive PREFIX only (`c:` ==
|
|
283
|
+
`C:`) and compares every other component exactly; mirrored here. In practice
|
|
284
|
+
both values come from the same `abspath`, so it only matters against a
|
|
285
|
+
hand-written manifest.
|
|
286
|
+
"""
|
|
287
|
+
root = workspace_root.replace("\\", "/").rstrip("/")
|
|
288
|
+
probe = path.replace("\\", "/")
|
|
289
|
+
if os.name == "nt":
|
|
290
|
+
root_drive, root_rest = os.path.splitdrive(root)
|
|
291
|
+
probe_drive, probe_rest = os.path.splitdrive(probe)
|
|
292
|
+
matched = root_drive.lower() == probe_drive.lower() and probe_rest.startswith(
|
|
293
|
+
f"{root_rest}/"
|
|
294
|
+
)
|
|
295
|
+
else:
|
|
296
|
+
matched = probe.startswith(f"{root}/")
|
|
297
|
+
if not matched:
|
|
298
|
+
return path
|
|
299
|
+
return "${workspaceFolder}/" + path[len(root) + 1 :]
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def _load_yaml(path: Path) -> Any | None:
|
|
303
|
+
"""Parse a YAML file this project's build wrote, or `None`.
|
|
304
|
+
|
|
305
|
+
`None` for every failure -- missing file, unreadable, non-UTF-8, malformed
|
|
306
|
+
YAML, or PyYAML not installed. Both callers are best-effort by contract (see
|
|
307
|
+
[`_resolve_from_build`]): the field simply stays unresolved and the draft
|
|
308
|
+
keeps the placeholder it has always had.
|
|
309
|
+
|
|
310
|
+
tan ships no YAML dependency of its own, so PyYAML is used when importable
|
|
311
|
+
-- it always is in a Zephyr workspace, which is the only place a
|
|
312
|
+
`system-manifest.yaml` can exist -- and its absence degrades to "nothing
|
|
313
|
+
resolved" rather than failing a command whose whole point is to work before
|
|
314
|
+
the first build.
|
|
315
|
+
"""
|
|
316
|
+
try:
|
|
317
|
+
import yaml # noqa: PLC0415 (optional at runtime, by design)
|
|
318
|
+
except ImportError:
|
|
319
|
+
return None
|
|
320
|
+
try:
|
|
321
|
+
return yaml.safe_load(path.read_text(encoding="utf-8"))
|
|
322
|
+
except Exception: # noqa: BLE001 -- Zephyr's/the SDK's output, not ours
|
|
323
|
+
return None
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def _select_slice(
|
|
327
|
+
slices: list[dict[str, Any]], target: str, core: str | None
|
|
328
|
+
) -> dict[str, Any] | None:
|
|
329
|
+
"""The manifest slice a debug draft resolves against, for a target/`--core`.
|
|
330
|
+
|
|
331
|
+
`native-host` is a special case: its runnable artefact is the project's
|
|
332
|
+
`native_sim` slice, found by BOARD target -- the same discriminator `tan run`
|
|
333
|
+
uses to pick the host binary -- not by `os`. A board that also builds a real
|
|
334
|
+
Zephyr MCU slice still has one or more slices with `os: zephyr`; the old
|
|
335
|
+
`os`-keyed match took the first of those, which on such a board is often the
|
|
336
|
+
MCU slice, pointing `Alp: Native Sim Debug` at a Cortex-M ELF that CodeLLDB
|
|
337
|
+
then cannot launch. `--core` is intentionally unused on this arm: a
|
|
338
|
+
`native_sim` slice's `core_id` is not a hardware core selector.
|
|
339
|
+
"""
|
|
340
|
+
if target == NATIVE_HOST:
|
|
341
|
+
return next(
|
|
342
|
+
(s for s in slices if is_native_sim_board(s.get("board"))),
|
|
343
|
+
None,
|
|
344
|
+
)
|
|
345
|
+
manifest_os = MANIFEST_OS_BY_TARGET.get(target)
|
|
346
|
+
if manifest_os is None:
|
|
347
|
+
return None
|
|
348
|
+
# `--core` names the slice outright; otherwise the first slice of this
|
|
349
|
+
# target's OS wins, which is the whole manifest on a single-core project.
|
|
350
|
+
return next(
|
|
351
|
+
(
|
|
352
|
+
s
|
|
353
|
+
for s in slices
|
|
354
|
+
if s.get("os") == manifest_os and (core is None or s.get("core_id") == core)
|
|
355
|
+
),
|
|
356
|
+
None,
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def _slice_jlink_serial(slice_: dict[str, Any]) -> str | None:
|
|
361
|
+
"""The slice's `flash_args.jlink_serial` -- the probe `tan flash` selects --
|
|
362
|
+
or `None` when absent or malformed (validated the way `tan flash` does, so
|
|
363
|
+
a value it would refuse is never written into launch.json)."""
|
|
364
|
+
try:
|
|
365
|
+
serial = fa_str_checked(slice_.get("flash_args"), "jlink_serial", False)
|
|
366
|
+
if serial is not None:
|
|
367
|
+
validate_identifier(serial, "jlink_serial")
|
|
368
|
+
except FlashPlanError:
|
|
369
|
+
return None
|
|
370
|
+
return serial
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def _runner_arg_values(argv: Any, flag: str) -> list[str]:
|
|
374
|
+
"""Every value a runner's argv gives for `flag`, in either form west emits:
|
|
375
|
+
`--device=Cortex-M55` (inline) or `--config <path>` (separate token).
|
|
376
|
+
|
|
377
|
+
A list, not a single value: OpenOCD takes more than one `--config` and
|
|
378
|
+
cortex-debug's `configFiles` is an array -- collapsing to the first would
|
|
379
|
+
silently drop a board's second config file and produce a session that
|
|
380
|
+
attaches to half a target.
|
|
381
|
+
"""
|
|
382
|
+
if not isinstance(argv, list):
|
|
383
|
+
return []
|
|
384
|
+
tokens = [a for a in argv if isinstance(a, str)]
|
|
385
|
+
inline = f"{flag}="
|
|
386
|
+
values: list[str] = []
|
|
387
|
+
i = 0
|
|
388
|
+
while i < len(tokens):
|
|
389
|
+
arg = tokens[i]
|
|
390
|
+
if arg.startswith(inline):
|
|
391
|
+
value = arg[len(inline) :]
|
|
392
|
+
if value:
|
|
393
|
+
values.append(value)
|
|
394
|
+
elif arg == flag:
|
|
395
|
+
# The next token is the value only when it is not another flag -- a
|
|
396
|
+
# trailing `--config` with nothing after it must not swallow the
|
|
397
|
+
# following option as if it were a path.
|
|
398
|
+
if i + 1 < len(tokens) and not tokens[i + 1].startswith("-"):
|
|
399
|
+
values.append(tokens[i + 1])
|
|
400
|
+
i += 1
|
|
401
|
+
i += 1
|
|
402
|
+
return values
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def _str_or_none(value: Any) -> str | None:
|
|
406
|
+
"""A non-empty string, else `None`. An empty string in Zephyr's own output
|
|
407
|
+
means "absent", not a path (`runners.rs::non_empty`)."""
|
|
408
|
+
return value if isinstance(value, str) and value != "" else None
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def _str_list(value: Any) -> list[str]:
|
|
412
|
+
return [v for v in value if isinstance(v, str)] if isinstance(value, list) else []
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
def _resolve_from_build(
|
|
416
|
+
workspace_root: str, target: str, server: str, core: str | None
|
|
417
|
+
) -> tuple[LaunchResolution, list[str], str | None]:
|
|
418
|
+
"""Everything this project's own build knows about how to debug it: the
|
|
419
|
+
per-core ELF from `build/system-manifest.yaml`, and the probe/tool paths from
|
|
420
|
+
that slice's `runners.yaml` -- the same file `west flash` reads.
|
|
421
|
+
|
|
422
|
+
Best-effort throughout. A missing manifest (pre-build), a missing slice, an
|
|
423
|
+
unreadable or reshaped `runners.yaml` each leave the corresponding field
|
|
424
|
+
unresolved instead of failing the command: `debug-config` must still emit its
|
|
425
|
+
draft before the first build. Returns the resolution, the runner ids the
|
|
426
|
+
board registered (for the "this build registers no such runner" note), and
|
|
427
|
+
the `core_id` of the slice this run actually selected (`None` before a
|
|
428
|
+
matching slice is found) -- the SAME id `--core` would have named
|
|
429
|
+
explicitly. alp-sdk#1026's SDK-metadata fallback
|
|
430
|
+
(`_fill_debug_probe_identity_from_sdk`) needs it to index `jlink_device`
|
|
431
|
+
(keyed per core) even when the caller passed no `--core` of its own, so a
|
|
432
|
+
single-core project's ALREADY-built slice still resolves without forcing
|
|
433
|
+
the user to repeat a core id `tan` already knows.
|
|
434
|
+
"""
|
|
435
|
+
resolution = LaunchResolution()
|
|
436
|
+
manifest = _load_yaml(Path(workspace_root, "build", "system-manifest.yaml"))
|
|
437
|
+
slice_ = _select_slice(manifest_slices(manifest), target, core)
|
|
438
|
+
if slice_ is None:
|
|
439
|
+
return resolution, [], None
|
|
440
|
+
core_id = slice_.get("core_id")
|
|
441
|
+
core_id = core_id if isinstance(core_id, str) else None
|
|
442
|
+
|
|
443
|
+
artefact = _str_or_none(slice_.get("output_artefact"))
|
|
444
|
+
if artefact is not None:
|
|
445
|
+
# A host target needs the sibling swap; every other target kind
|
|
446
|
+
# genuinely wants the artefact verbatim.
|
|
447
|
+
if target == NATIVE_HOST:
|
|
448
|
+
artefact = native_sim_exe_beside(artefact)
|
|
449
|
+
resolution.executable = _workspace_relative(workspace_root, artefact)
|
|
450
|
+
|
|
451
|
+
if server == JLINK:
|
|
452
|
+
resolution.probe_serial = _slice_jlink_serial(slice_)
|
|
453
|
+
|
|
454
|
+
build_dir = _str_or_none(slice_.get("build_dir"))
|
|
455
|
+
if build_dir is None:
|
|
456
|
+
return resolution, [], core_id
|
|
457
|
+
runners = _load_yaml(Path(build_dir, "zephyr", "runners.yaml"))
|
|
458
|
+
if not isinstance(runners, dict):
|
|
459
|
+
return resolution, [], core_id
|
|
460
|
+
config = runners.get("config")
|
|
461
|
+
config = config if isinstance(config, dict) else {}
|
|
462
|
+
args = runners.get("args")
|
|
463
|
+
args = args if isinstance(args, dict) else {}
|
|
464
|
+
|
|
465
|
+
resolution.gdb_path = _str_or_none(config.get("gdb"))
|
|
466
|
+
runner = _RUNNER_ID.get(server)
|
|
467
|
+
if runner is not None:
|
|
468
|
+
if server == JLINK:
|
|
469
|
+
values = _runner_arg_values(args.get(runner), "--device")
|
|
470
|
+
resolution.device = values[0] if values else None
|
|
471
|
+
elif server == OPENOCD:
|
|
472
|
+
resolution.server_path = _str_or_none(config.get("openocd"))
|
|
473
|
+
resolution.search_dirs = _str_list(config.get("openocd_search"))
|
|
474
|
+
resolution.config_files = _runner_arg_values(args.get(runner), "--config")
|
|
475
|
+
elif server == PYOCD:
|
|
476
|
+
values = _runner_arg_values(args.get(runner), "--target")
|
|
477
|
+
resolution.target_id = values[0] if values else None
|
|
478
|
+
return resolution, _str_list(runners.get("runners")), core_id
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
def _sdk_variant_debug_block(
|
|
482
|
+
sdk_root: str, sku: str, *, warnings: list[str] | None = None,
|
|
483
|
+
skipped: list[str] | None = None,
|
|
484
|
+
) -> dict[str, Any] | None:
|
|
485
|
+
"""The resolved SoC-JSON `variants[].debug` block for `sku`'s SoM preset,
|
|
486
|
+
or `None` when any step of the walk fails to resolve one.
|
|
487
|
+
|
|
488
|
+
The metadata-layout walk itself is `build_output.read_sdk_som_and_soc` --
|
|
489
|
+
the ONE reader `tan size` also drives, not a second one that could
|
|
490
|
+
disagree (the exact drift alp-sdk#1026 itself is about).
|
|
491
|
+
|
|
492
|
+
A forward-only `resolve_variant` match: `sku` is passed as `None`,
|
|
493
|
+
deliberately disabling the reverse `sku in alp_module_skus` fallback
|
|
494
|
+
`tan size` itself relies on -- a drifted/`TBD` preset must resolve NO
|
|
495
|
+
identity rather than possibly a WRONG one that still connects a live debug
|
|
496
|
+
session to the wrong part (alp-sdk#1026 review finding #7).
|
|
497
|
+
|
|
498
|
+
*warnings* (tan-cli#964 review, major 5): threaded straight through to
|
|
499
|
+
`read_sdk_som_and_soc`, which threads it into both leaf readers'
|
|
500
|
+
`validate_document` calls -- the same `metadata_root`/`warnings`
|
|
501
|
+
convention `tan size` already uses. Before this, `tan debug-config`
|
|
502
|
+
called `read_sdk_som_and_soc` with no `warnings` at all, so it validated
|
|
503
|
+
NOTHING despite the PR body's own claim that it inherited #964's WARN
|
|
504
|
+
half "transitively" -- it did not, until this parameter existed.
|
|
505
|
+
|
|
506
|
+
*skipped* (tan-cli#964 review, major 6): same threading, for the
|
|
507
|
+
"skip-but-disclose" collector -- a note when the schema file itself is
|
|
508
|
+
simply absent, rather than the silent `[]` that used to be.
|
|
509
|
+
"""
|
|
510
|
+
metadata_root = os.path.join(sdk_root, "metadata")
|
|
511
|
+
walked = read_sdk_som_and_soc(
|
|
512
|
+
metadata_root, sku, warnings=warnings, skipped=skipped, explain_unsupported=True
|
|
513
|
+
)
|
|
514
|
+
if walked is None:
|
|
515
|
+
return None
|
|
516
|
+
_silicon, silicon_variant, variants, _soc_flash_mb, _soc_cores = walked
|
|
517
|
+
variant = resolve_variant(silicon_variant, None, variants)
|
|
518
|
+
if variant is None:
|
|
519
|
+
return None
|
|
520
|
+
debug = variant.get("debug")
|
|
521
|
+
return debug if isinstance(debug, dict) else None
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
def _board_som_sku(board_yaml_path: str) -> str | None:
|
|
525
|
+
"""This project's `board.yaml` `som.sku`, or `None` for every failure --
|
|
526
|
+
missing/unreadable file, no PyYAML, not a mapping, no `som.sku`, an empty
|
|
527
|
+
one. Extracted so the tan-cli#477 `--core` guard and
|
|
528
|
+
[`_fill_debug_probe_identity_from_sdk`] read the SAME field the same
|
|
529
|
+
tolerant way rather than each spelling the four-step `isinstance` walk out
|
|
530
|
+
again (they used to be one site; the guard needs it strictly earlier)."""
|
|
531
|
+
board = _load_yaml(Path(board_yaml_path))
|
|
532
|
+
som = board.get("som") if isinstance(board, dict) else None
|
|
533
|
+
sku = som.get("sku") if isinstance(som, dict) else None
|
|
534
|
+
return sku if isinstance(sku, str) and sku != "" else None
|
|
535
|
+
|
|
536
|
+
|
|
537
|
+
def _sdk_published_cores(
|
|
538
|
+
sdk_root: str | None, board_yaml_path: str, *, warnings: list[str] | None = None,
|
|
539
|
+
skipped: list[str] | None = None,
|
|
540
|
+
) -> frozenset[str]:
|
|
541
|
+
"""Every core id this project's SoM publishes, per the SDK -- the SoC
|
|
542
|
+
JSON's own `cores[].id`, unioned with the `variants[].debug.jlink_device`
|
|
543
|
+
keys. Empty whenever the walk resolves nothing, which the caller reads as
|
|
544
|
+
"cannot be asked", never as "this SoM has no cores".
|
|
545
|
+
|
|
546
|
+
*warnings*/*skipped*: see `_sdk_variant_debug_block`'s own doc -- same
|
|
547
|
+
threading, same reason (tan-cli#964 review, majors 5/6).
|
|
548
|
+
|
|
549
|
+
tan-cli#477 major 2. `--core` pre-build is NOT decoration: with no
|
|
550
|
+
`build/system-manifest.yaml` to check against, it selects which core's
|
|
551
|
+
SDK-published debug-probe identity to resolve (alp-sdk#1026), so a guard
|
|
552
|
+
keyed only on "does a build manifest exist" cannot tell a typo apart from
|
|
553
|
+
that legitimate use -- which is exactly why #508 left this half open. It
|
|
554
|
+
CAN be told apart by consulting the SDK's own published core list, and
|
|
555
|
+
that list is already reachable here from the same `--sdk-root` the
|
|
556
|
+
identity fallback below reads.
|
|
557
|
+
|
|
558
|
+
`cores[].id` is the authority; the `jlink_device` union only widens it for
|
|
559
|
+
a SoC JSON that omits `cores` entirely. Measured across every SoC JSON in
|
|
560
|
+
alp-sdk `metadata/socs/**` (alif e3-e8, deepx dx/m1,
|
|
561
|
+
renesas rzv2n/n44): the `jlink_device` keys are a SUBSET of `cores[].id`
|
|
562
|
+
in all of them, so on real metadata the union IS `cores[].id`. It is kept
|
|
563
|
+
because the frozen contract fixture
|
|
564
|
+
`contract/envelopes/debug-config-preview-zephyr-mcu-sdk-identity/sdk` --
|
|
565
|
+
which cannot be edited -- publishes `jlink_device` and no `cores` at all,
|
|
566
|
+
and its `--core m55_hp` must not become a false refusal.
|
|
567
|
+
|
|
568
|
+
Same vocabulary either side: a `build/system-manifest.yaml` slice's
|
|
569
|
+
`core_id` is spelled with these same ids (`m55_hp`, `m33_sm`,
|
|
570
|
+
`a55_cluster`), which is what lets this stand in for the manifest check
|
|
571
|
+
when there is no manifest.
|
|
572
|
+
"""
|
|
573
|
+
if sdk_root is None:
|
|
574
|
+
return frozenset()
|
|
575
|
+
sku = _board_som_sku(board_yaml_path)
|
|
576
|
+
if sku is None:
|
|
577
|
+
return frozenset()
|
|
578
|
+
walked = read_sdk_som_and_soc(
|
|
579
|
+
os.path.join(sdk_root, "metadata"), sku, warnings=warnings, skipped=skipped,
|
|
580
|
+
explain_unsupported=True,
|
|
581
|
+
)
|
|
582
|
+
if walked is None:
|
|
583
|
+
return frozenset()
|
|
584
|
+
_silicon, silicon_variant, variants, _soc_flash_mb, soc_cores = walked
|
|
585
|
+
cores = {core_id for core_id, _tcm_kb in soc_cores}
|
|
586
|
+
variant = resolve_variant(silicon_variant, None, variants)
|
|
587
|
+
debug = variant.get("debug") if isinstance(variant, dict) else None
|
|
588
|
+
jlink_device = debug.get("jlink_device") if isinstance(debug, dict) else None
|
|
589
|
+
if isinstance(jlink_device, dict):
|
|
590
|
+
cores |= {k for k in jlink_device if isinstance(k, str)}
|
|
591
|
+
return frozenset(cores)
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
def _sdk_core_refusal_authority(
|
|
595
|
+
sdk_root: str | None, sdk_source_tier: str, foreign_global_default_for: str | None
|
|
596
|
+
) -> str | None:
|
|
597
|
+
"""The SDK root that is allowed to REFUSE a `--core`, or `None`.
|
|
598
|
+
|
|
599
|
+
tan-cli#477 major 2, REVIEW round. The refusal below is decided by an SDK
|
|
600
|
+
checkout, and until this guard existed that could be a checkout the user
|
|
601
|
+
never named: with no `--sdk-root` and no project pin, `resolve_sdk_tiered`
|
|
602
|
+
falls through to the machine-global default (`~/.alp/sdk-default`), which
|
|
603
|
+
`tan bootstrap` may have last pointed at an unrelated project. Measured on
|
|
604
|
+
this box, from a project directory that names no SDK at all:
|
|
605
|
+
|
|
606
|
+
resolve_project_context('.', None, None).sdk
|
|
607
|
+
-> SdkInfo(root='.../alp-workspace/sdk-triage',
|
|
608
|
+
source_tier='globalDefault',
|
|
609
|
+
foreign_global_default_for='.../t477/p')
|
|
610
|
+
|
|
611
|
+
and the refusal flips purely on which checkout answers -- measured, same
|
|
612
|
+
project, same `--core m55_hp`, two SDK roots differing only in `e8.json`'s
|
|
613
|
+
`cores[]`: `--sdk-root A` -> exit 0, `--sdk-root B` -> exit 2
|
|
614
|
+
`debug-config.core-unknown`. `debug-config` reports no `sdk` block at all
|
|
615
|
+
(deliberately -- see [`_resolve_project_reporting_fields`]) and does not
|
|
616
|
+
emit the `sdk.global-default-foreign-project` warning `size`/`image` emit
|
|
617
|
+
for this same situation, so a customer would have no way to see that a
|
|
618
|
+
stranger's checkout had just refused their build.
|
|
619
|
+
|
|
620
|
+
The guard's own rule -- "refuse only what you can PROVE unknown" -- settles
|
|
621
|
+
it. A global default last pinned by ANOTHER project cannot prove anything
|
|
622
|
+
about THIS project's SoM, so it declines to `None`, which the caller reads
|
|
623
|
+
as "cannot be asked" and stays silent, exactly as it does with no SDK at
|
|
624
|
+
all. That single flag is the whole discriminator on purpose: every other
|
|
625
|
+
way to arrive here -- `--sdk-root`, a working project pin, or a global
|
|
626
|
+
default THIS project's own bootstrap set (including after a broken pin fell
|
|
627
|
+
through to it) -- is an SDK this project is entitled to be judged by. Those
|
|
628
|
+
still refuse, and the refusal now names the checkout and the tier that
|
|
629
|
+
decided it.
|
|
630
|
+
"""
|
|
631
|
+
if sdk_root is None:
|
|
632
|
+
return None
|
|
633
|
+
if foreign_global_default_for is not None:
|
|
634
|
+
return None
|
|
635
|
+
return sdk_root
|
|
636
|
+
|
|
637
|
+
|
|
638
|
+
def _sdk_core_unknown_message(
|
|
639
|
+
core: str, published: frozenset[str], sdk_root: str, sdk_source_tier: str
|
|
640
|
+
) -> str:
|
|
641
|
+
"""The tan-cli#477 counterpart of `debug_launch.explicit_core_unknown_
|
|
642
|
+
message`, which names the cores a BUILD produced. There is no build here,
|
|
643
|
+
so it names the cores the SDK publishes instead -- same refusal, same
|
|
644
|
+
code, a different (and the only available) authority.
|
|
645
|
+
|
|
646
|
+
It names WHICH checkout, and by which tier it was chosen (REVIEW round).
|
|
647
|
+
The build-manifest arm needs no such line -- the manifest is inside the
|
|
648
|
+
project the user pointed at -- but this arm's authority may be a checkout
|
|
649
|
+
the argv never mentions (`--sdk-root` absent, a project pin, or a global
|
|
650
|
+
default), and `debug-config` publishes no `sdk` envelope block for the
|
|
651
|
+
reader to look it up in. A refusal a customer cannot attribute is a support
|
|
652
|
+
ticket; naming the path makes `--sdk-root <the right one>` the obvious next
|
|
653
|
+
move. See [`_sdk_core_refusal_authority`] for the tier that is NOT allowed
|
|
654
|
+
to get this far."""
|
|
655
|
+
cores = ", ".join(sorted(published))
|
|
656
|
+
return (
|
|
657
|
+
f"--core {core} names no core this project's SoM has (its cores, per "
|
|
658
|
+
f"the SDK's published metadata for this board.yaml's som.sku: "
|
|
659
|
+
f"{cores}), and this project has no build/system-manifest.yaml to "
|
|
660
|
+
"check against instead. Pass a --core value this SoM actually has. "
|
|
661
|
+
f"That core list came from the alp-sdk checkout at {sdk_root} "
|
|
662
|
+
f"(resolved by: {sdk_source_tier}) -- if that is not the checkout this "
|
|
663
|
+
"project should be judged against, pass --sdk-root explicitly. "
|
|
664
|
+
"Left unrefused, this wrote a launch.json whose device stayed the "
|
|
665
|
+
"literal <resolved-device> placeholder at exit 0 -- a file that looks "
|
|
666
|
+
"valid and fails later in the debugger (tan-cli#477)."
|
|
667
|
+
)
|
|
668
|
+
|
|
669
|
+
|
|
670
|
+
def _fill_debug_probe_identity_from_sdk(
|
|
671
|
+
resolution: LaunchResolution,
|
|
672
|
+
sdk_root: str | None,
|
|
673
|
+
board_yaml_path: str,
|
|
674
|
+
core_id: str | None,
|
|
675
|
+
*,
|
|
676
|
+
warnings: list[str] | None = None,
|
|
677
|
+
skipped: list[str] | None = None,
|
|
678
|
+
) -> tuple[bool, frozenset[str]]:
|
|
679
|
+
"""alp-sdk#1026: fill `resolution`'s remaining `device`/`target_id`/
|
|
680
|
+
`config_files` gaps from the SDK's published per-variant debug-probe
|
|
681
|
+
identity (`variants[].debug`, alp-sdk#987), so `tan debug-config` resolves
|
|
682
|
+
a real J-Link device / pyOCD target before the project has ever been built
|
|
683
|
+
-- the case [`_resolve_from_build`]'s `runners.yaml` read structurally
|
|
684
|
+
cannot cover. Port of
|
|
685
|
+
`crates/tan-cli/src/commands/debug_config.rs::fill_debug_probe_identity_from_sdk`.
|
|
686
|
+
|
|
687
|
+
Best-effort throughout, exactly like [`_resolve_from_build`]: a missing
|
|
688
|
+
`board.yaml`/`som.sku`, no resolved SDK root, a missing/unreadable SoM
|
|
689
|
+
preset or SoC-JSON file, or a SoC variant that resolves but declares no
|
|
690
|
+
`debug` block each leave `resolution` exactly as it was -- the caller's
|
|
691
|
+
existing placeholder note still applies, and nothing here can fail the
|
|
692
|
+
command.
|
|
693
|
+
|
|
694
|
+
Returns `(debug_block_found, known_jlink_cores)`. `debug_block_found` is
|
|
695
|
+
whether a `variants[].debug` block was actually found for the resolved SoC
|
|
696
|
+
variant -- distinct from whether every field this run wanted got filled
|
|
697
|
+
from it. The caller uses this (alp-sdk#1026 review finding #4) to tell
|
|
698
|
+
"the SDK publishes an identity for this part, but not a value for the
|
|
699
|
+
specific field this server needs yet" apart from "no identity was
|
|
700
|
+
resolvable at all". `known_jlink_cores` is `jlink_device`'s own key set --
|
|
701
|
+
tan-cli#489 (4): `jlink_device` is the ONE field here keyed by core
|
|
702
|
+
(`pyocd_target`/`openocd_config` are not), so a `device` placeholder that
|
|
703
|
+
survives despite `debug_block_found` being true is either "no core id was
|
|
704
|
+
resolved to index with" or "the given core id has no entry in this SoM's
|
|
705
|
+
published map" -- two distinct, both-fixable-by-`--core` causes the caller
|
|
706
|
+
cannot tell apart, or name the known cores for, without this set.
|
|
707
|
+
|
|
708
|
+
*warnings*/*skipped*: see `_sdk_variant_debug_block`'s own doc -- same
|
|
709
|
+
threading, same reason (tan-cli#964 review, majors 5/6).
|
|
710
|
+
"""
|
|
711
|
+
if sdk_root is None:
|
|
712
|
+
return False, frozenset()
|
|
713
|
+
sku = _board_som_sku(board_yaml_path)
|
|
714
|
+
if sku is None:
|
|
715
|
+
return False, frozenset()
|
|
716
|
+
debug = _sdk_variant_debug_block(sdk_root, sku, warnings=warnings, skipped=skipped)
|
|
717
|
+
if debug is None:
|
|
718
|
+
return False, frozenset()
|
|
719
|
+
jlink_device = debug.get("jlink_device")
|
|
720
|
+
jlink_device = (
|
|
721
|
+
{k: v for k, v in jlink_device.items() if isinstance(v, str)}
|
|
722
|
+
if isinstance(jlink_device, dict)
|
|
723
|
+
else {}
|
|
724
|
+
)
|
|
725
|
+
pyocd_target = debug.get("pyocd_target")
|
|
726
|
+
pyocd_target = pyocd_target if isinstance(pyocd_target, str) else None
|
|
727
|
+
openocd_config = debug.get("openocd_config")
|
|
728
|
+
openocd_config = openocd_config if isinstance(openocd_config, str) else None
|
|
729
|
+
fill_debug_probe_identity_gaps(resolution, core_id, jlink_device, pyocd_target, openocd_config)
|
|
730
|
+
return True, frozenset(jlink_device.keys())
|
|
731
|
+
|
|
732
|
+
|
|
733
|
+
def _resolve_user_svd(workspace_root: str, arg: str) -> str:
|
|
734
|
+
"""Resolve `--svd` into the value the launch configuration should carry.
|
|
735
|
+
|
|
736
|
+
**Anchor: the current directory, not the project root.** `--svd` is a
|
|
737
|
+
per-invocation flag typed at a shell prompt, so a relative path means what
|
|
738
|
+
the shell means by it. (A board-level `debug.svd` key, should one ever be
|
|
739
|
+
added, travels with the project and must anchor on the project root instead
|
|
740
|
+
-- the two have different lifetimes, so they get different anchors
|
|
741
|
+
deliberately rather than by omission.) The emitted string then goes through
|
|
742
|
+
the same [`_workspace_relative`] rewrite as `executable`: inside the project
|
|
743
|
+
it becomes `${workspaceFolder}/...` so a committed launch.json stays
|
|
744
|
+
portable, outside it stays absolute -- the normal case, since a vendor SVD
|
|
745
|
+
lives in the vendor SDK the user installed.
|
|
746
|
+
|
|
747
|
+
**A bad path is a HARD ERROR, never a silent drop back to "no SVD".**
|
|
748
|
+
tan-cli#67 established that cortex-debug fails the whole session on an
|
|
749
|
+
`svdFile` it cannot read, which is why the *unresolved* case drops the key.
|
|
750
|
+
But the user explicitly named this file: falling back would make a typo
|
|
751
|
+
indistinguishable from not passing the flag, and the failure would surface as
|
|
752
|
+
an unexplained empty peripheral view.
|
|
753
|
+
"""
|
|
754
|
+
if arg.strip() == "":
|
|
755
|
+
raise DebugConfigError("Alp: --svd was given an empty path.")
|
|
756
|
+
# `abspath` on an absolute `arg` returns it normalised, so this handles both.
|
|
757
|
+
candidate = _normalise(arg)
|
|
758
|
+
# `stat` then S_ISREG, mirroring the Rust's `metadata()`-then-`is_file()`
|
|
759
|
+
# pair: the two failures get DIFFERENT messages, and a bare `is_file()`
|
|
760
|
+
# (false for both a missing path and a directory) could not tell them apart.
|
|
761
|
+
try:
|
|
762
|
+
mode = os.stat(candidate).st_mode
|
|
763
|
+
except OSError as err:
|
|
764
|
+
raise DebugConfigError(
|
|
765
|
+
f"Alp: --svd path cannot be read: {candidate} ({err}). "
|
|
766
|
+
"Pass the path to the vendor's own .svd file; the SDK ships none "
|
|
767
|
+
"(alp-sdk#948)."
|
|
768
|
+
) from err
|
|
769
|
+
if not stat.S_ISREG(mode):
|
|
770
|
+
raise DebugConfigError(f"Alp: --svd path is not a file: {candidate}")
|
|
771
|
+
return _workspace_relative(workspace_root, candidate)
|
|
772
|
+
|
|
773
|
+
|
|
774
|
+
def _resolve_gdbserver_address(arg: str) -> str:
|
|
775
|
+
"""Validate `--gdbserver-address` (tan-cli#321). Emitted verbatim into
|
|
776
|
+
`miDebuggerServerAddress` -- cppdbg accepts a bare hostname, an IPv4 or
|
|
777
|
+
bracketed-IPv6 literal, so there is no single `host:port` shape narrow
|
|
778
|
+
enough to validate without rejecting a real one; the only input that can
|
|
779
|
+
never be a real address is an empty string, the same floor `--svd` holds
|
|
780
|
+
for its own path argument.
|
|
781
|
+
"""
|
|
782
|
+
if arg.strip() == "":
|
|
783
|
+
raise DebugConfigError("Alp: --gdbserver-address was given an empty value.")
|
|
784
|
+
return arg
|
|
785
|
+
|
|
786
|
+
|
|
787
|
+
def _gdbserver_address_unresolved_issue() -> Issue:
|
|
788
|
+
"""tan-cli#321 direction 1: the yocto-userspace draft's
|
|
789
|
+
`miDebuggerServerAddress` is still the unresolved `<host>:<port>`
|
|
790
|
+
placeholder in what this run actually produced. Severity `info` -- this is
|
|
791
|
+
not a failure, it is the one field on this target class that NO build and
|
|
792
|
+
NO SDK-published metadata can ever resolve (it names where the board ends
|
|
793
|
+
up after deploy, a fact that exists only at runtime), so surfacing it
|
|
794
|
+
explicitly is the whole point of this issue rather than leaving F5 to fail
|
|
795
|
+
silently at connect.
|
|
796
|
+
|
|
797
|
+
tan-cli#138 vs #321: unlike the other three target classes, this profile's
|
|
798
|
+
`preLaunchTask` carries NO restored default -- see
|
|
799
|
+
`tan.core.debug_launch.DEFAULT_PRE_LAUNCH_TASK`'s own doc comment for why:
|
|
800
|
+
alp-sdk-vscode registers no working task for yocto-userspace (the only one
|
|
801
|
+
that exists exits 1 by design), so naming one here would put the
|
|
802
|
+
"preLaunchTask terminated with exit code 1" dialog in front of every F5.
|
|
803
|
+
Said here, alongside the address gap, rather than as a second issue: both
|
|
804
|
+
point at the same manual deploy-and-start-gdbserver step, and a customer
|
|
805
|
+
who wants a reminder can still opt one in explicitly.
|
|
806
|
+
"""
|
|
807
|
+
return Issue(
|
|
808
|
+
"debug-config.gdbserver-address-unresolved",
|
|
809
|
+
"info",
|
|
810
|
+
"This yocto-userspace configuration's `miDebuggerServerAddress` is "
|
|
811
|
+
"still the placeholder `<host>:<port>` -- the host and gdbserver port "
|
|
812
|
+
"are a runtime property of the deployed board that no build can "
|
|
813
|
+
"resolve. Fill it in by hand in launch.json once you know it, or pass "
|
|
814
|
+
"`--gdbserver-address host:port` next time you regenerate this "
|
|
815
|
+
"profile. tan has no deploy mechanism of its own, so deploying the "
|
|
816
|
+
"binary and starting gdbserver on the target before F5 is still a "
|
|
817
|
+
"manual step; this profile carries no `preLaunchTask` reminder of "
|
|
818
|
+
"that by default (tan-cli#138 vs #321 -- the extension's only "
|
|
819
|
+
"registered task for this target exits 1 by design, so naming it "
|
|
820
|
+
"would fail before every F5). Pass `--pre-launch-task '<name>'` to "
|
|
821
|
+
"add a reminder of your own.",
|
|
822
|
+
)
|
|
823
|
+
|
|
824
|
+
|
|
825
|
+
def _has_placeholder(value: Any) -> bool:
|
|
826
|
+
"""Whether any `<...>` placeholder survived resolution, anywhere in the draft
|
|
827
|
+
-- including inside `configFiles`, which is an array.
|
|
828
|
+
|
|
829
|
+
The string test is [`is_unresolved_placeholder`], the SAME predicate the
|
|
830
|
+
launch.json merge uses, so "keep the still-needs-resolution note" and "do not
|
|
831
|
+
overwrite this hand-filled value" can never disagree. It used to be
|
|
832
|
+
`contains("<resolved-")`, which called the two-token `<host>:<port>` a real
|
|
833
|
+
address: a yocto config whose `<resolved-gdb>` resolved then dropped the note
|
|
834
|
+
while `miDebuggerServerAddress` was still unusable.
|
|
835
|
+
"""
|
|
836
|
+
if isinstance(value, str):
|
|
837
|
+
return is_unresolved_placeholder(value)
|
|
838
|
+
if isinstance(value, list):
|
|
839
|
+
return any(_has_placeholder(v) for v in value)
|
|
840
|
+
if isinstance(value, dict):
|
|
841
|
+
return any(_has_placeholder(v) for v in value.values())
|
|
842
|
+
return False
|
|
843
|
+
|
|
844
|
+
|
|
845
|
+
# ---------------------------------------------------------------------------
|
|
846
|
+
# tan-cli#1179: the OpenOCD-without-host-tools note.
|
|
847
|
+
# tan-cli#1194 review: the note's WORDING, split three ways so each spelling
|
|
848
|
+
# is true in the state it is emitted in.
|
|
849
|
+
#
|
|
850
|
+
# THE SILENT SHAPE THIS CLOSES. Zephyr writes `config.openocd` /
|
|
851
|
+
# `config.openocd_search` into `runners.yaml` only inside `if(OPENOCD)`
|
|
852
|
+
# (`cmake/flash/CMakeLists.txt`), and `OPENOCD` is a bare, optional
|
|
853
|
+
# `find_program(OPENOCD openocd)` (`cmake/modules/FindHostTools.cmake`, whose
|
|
854
|
+
# own comment reads "openocd is an optional dependency") that normally finds
|
|
855
|
+
# the SDK's copy through `list(APPEND CMAKE_PREFIX_PATH ${HOST_TOOLS_HOME}/usr)`
|
|
856
|
+
# in the SDK's own `cmake/zephyr/host-tools.cmake`. Since tan-cli#1176/#1178
|
|
857
|
+
# `tan bootstrap` passes `--no-hosttools`, so there is no `hosttools/` to find.
|
|
858
|
+
# CMake's `if()` reads `OPENOCD-NOTFOUND` as false, so both keys are ABSENT
|
|
859
|
+
# rather than present-and-empty; both `serverpath` and `searchDir` are ADDITIVE
|
|
860
|
+
# keys in `apply_launch_resolution`, and neither appears as a `<resolved-...>`
|
|
861
|
+
# placeholder in the OpenOCD draft, so `_has_placeholder` never sees them and
|
|
862
|
+
# the "Placeholder fields" note never fires. Contrast `svdFile`/`svdPath`,
|
|
863
|
+
# which ARE placeholders and ARE deleted -- that is the path that would have
|
|
864
|
+
# warned. `NO_HOSTTOOLS_FLAG` in `tan.core.toolchain_provision` is where the
|
|
865
|
+
# flag itself is decided, and records the same degradation from that side.
|
|
866
|
+
#
|
|
867
|
+
# A NOTE, NOT A FAILURE. The emitted configuration is valid and `configFiles`
|
|
868
|
+
# still resolves (from `board.cmake`, not from `hosttools/`). What is missing
|
|
869
|
+
# is one advisory field, so this rides in `data.notes` and `tan debug-config`
|
|
870
|
+
# still exits 0 with `ok: true`.
|
|
871
|
+
#
|
|
872
|
+
# THREE CONDITIONS, ALL LOAD-BEARING -- see `doctor_cmd.west_check`'s own
|
|
873
|
+
# account of the lesson ("a warning that fires on every correct install trains
|
|
874
|
+
# users to ignore warnings"):
|
|
875
|
+
#
|
|
876
|
+
# * `server` AND the draft's `servertype` are both OpenOCD. `server` is what
|
|
877
|
+
# the caller asked for; `servertype` is what the FINAL draft emits, and a
|
|
878
|
+
# yocto-userspace (cppdbg) draft has no `serverpath` field to be missing in
|
|
879
|
+
# the first place -- warning there would be noise about a key that target
|
|
880
|
+
# kind never carries.
|
|
881
|
+
# * `serverpath` is absent from the draft. That is the resolution's own
|
|
882
|
+
# answer, read the same way the rest of `_preview_notes_for` reads the final
|
|
883
|
+
# draft: `apply_launch_resolution` inserts the key if and only if
|
|
884
|
+
# `runners.yaml` gave it one.
|
|
885
|
+
# * No `openocd` on PATH. This is what keeps a correctly-provisioned host
|
|
886
|
+
# quiet, including the MIXED host measured in tan-cli#1179 (system OpenOCD
|
|
887
|
+
# on PATH, no `hosttools/`), which is silenced twice over: with
|
|
888
|
+
# `find_program` succeeding, `runners.yaml` DOES carry `config.openocd` --
|
|
889
|
+
# alongside an `openocd_search` pointing at the SDK's nonexistent
|
|
890
|
+
# `share/openocd/scripts`, from `set_ifndef(OPENOCD_DEFAULT_PATH ...)`,
|
|
891
|
+
# measured benign because OpenOCD still loads off its built-in scripts
|
|
892
|
+
# directory -- so `serverpath` is present AND the probe finds one. A host
|
|
893
|
+
# that has OpenOCD but built before installing it stays quiet too:
|
|
894
|
+
# cortex-debug's own PATH lookup will find it, so there is nothing to tell
|
|
895
|
+
# that developer.
|
|
896
|
+
#
|
|
897
|
+
# WHICH WORDING (tan-cli#1194 review). The first spelling of this note
|
|
898
|
+
# asserted "this project's runners.yaml has no 'config.openocd' key" in EVERY
|
|
899
|
+
# state it fired in, including the state it fires in most often: before the
|
|
900
|
+
# first build, where `_resolve_from_build` returns at its missing-manifest
|
|
901
|
+
# branch and never opens a `runners.yaml` at all. That is the command's own
|
|
902
|
+
# primary documented use ("`debug-config` must still emit its draft before
|
|
903
|
+
# the first build", `_resolve_from_build`'s docstring), and it is the state the
|
|
904
|
+
# `debug-config-preview-baremetal-mcu` contract golden records -- which
|
|
905
|
+
# alp-sdk-vscode surfaces to the user VERBATIM. So:
|
|
906
|
+
#
|
|
907
|
+
# * `registered_runners` is the "was a build read" signal, and it is the right
|
|
908
|
+
# one because `_resolve_from_build` returns `[]` from every branch that
|
|
909
|
+
# opened no readable `runners.yaml`: no manifest / no matching slice, a
|
|
910
|
+
# slice with no `build_dir`, and a `runners.yaml` that is missing or not a
|
|
911
|
+
# mapping. It over-approximates by exactly one contrived shape -- a
|
|
912
|
+
# `runners.yaml` that parses as a mapping but carries no `runners:` list --
|
|
913
|
+
# which is not a document any Zephyr build writes, and which by definition
|
|
914
|
+
# also carries no `config.openocd`, so both spellings describe a build that
|
|
915
|
+
# recorded nothing either way.
|
|
916
|
+
# * `baremetal-mcu` gets NEITHER Zephyr spelling. It is a plain-CMake, no-OS
|
|
917
|
+
# backend (`os: baremetal` -> `baremetal_cmake_flash`,
|
|
918
|
+
# `BAREMETAL_PROJECT_INCLUDE`), so no Zephyr SDK, no `find_program(OPENOCD
|
|
919
|
+
# openocd)` and no `--no-hosttools` install is in its story at all, and
|
|
920
|
+
# `_resolve_from_build` only ever reads `<build_dir>/zephyr/runners.yaml` --
|
|
921
|
+
# which that backend does not write. Handing that target kind a paragraph
|
|
922
|
+
# about the Zephyr SDK's host tools sends the reader to fix a thing that is
|
|
923
|
+
# not their problem. It still gets the actionable half (install `openocd`,
|
|
924
|
+
# put it on PATH, cortex-debug will find it), because a host with no
|
|
925
|
+
# `openocd` genuinely cannot start this session.
|
|
926
|
+
# * Only `zephyr-mcu` and `baremetal-mcu` can reach here at all:
|
|
927
|
+
# `SERVERS_BY_TARGET` offers `openocd` to those two and no others, and the
|
|
928
|
+
# `servertype` condition above already excludes every non-cortex-debug
|
|
929
|
+
# draft.
|
|
930
|
+
# ---------------------------------------------------------------------------
|
|
931
|
+
#: The half both Zephyr spellings end with: what to do about it. Factored out
|
|
932
|
+
#: so the advice has exactly one spelling and cannot drift between the two.
|
|
933
|
+
_OPENOCD_ZEPHYR_ADVICE = (
|
|
934
|
+
"tan bootstrap installs the Zephyr SDK with --no-hosttools, so the SDK "
|
|
935
|
+
"ships no OpenOCD of its own and Zephyr's optional find_program(OPENOCD "
|
|
936
|
+
"openocd) has none to record -- OpenOCD has to come from the system on "
|
|
937
|
+
"this host. Install it (your package manager's 'openocd', or a vendor "
|
|
938
|
+
"build), put it on PATH, and build this project so tan can fill "
|
|
939
|
+
"'serverpath' in; PATH alone is already enough for a session, because "
|
|
940
|
+
"cortex-debug falls back to its own PATH lookup when 'serverpath' is "
|
|
941
|
+
"absent."
|
|
942
|
+
)
|
|
943
|
+
|
|
944
|
+
#: A BUILD WAS READ and its `runners.yaml` carries no `config.openocd`. Kept
|
|
945
|
+
#: at module scope so the emitter and the tests that pin it have exactly one
|
|
946
|
+
#: spelling.
|
|
947
|
+
OPENOCD_NO_HOSTTOOLS_NOTE = (
|
|
948
|
+
"No OpenOCD executable could be resolved for this build, so this profile "
|
|
949
|
+
"carries no 'serverpath' (and no 'searchDir'): this project's runners.yaml "
|
|
950
|
+
"has no 'config.openocd' key, and no 'openocd' is on PATH here either. "
|
|
951
|
+
+ _OPENOCD_ZEPHYR_ADVICE
|
|
952
|
+
+ " Nothing else about this configuration is missing -- 'configFiles' "
|
|
953
|
+
"resolves from board.cmake, not from the SDK's host tools."
|
|
954
|
+
)
|
|
955
|
+
|
|
956
|
+
#: NO BUILD WAS READ -- the pre-build state, and the one the contract golden
|
|
957
|
+
#: records. Says there is no `runners.yaml` to have read a key out of, rather
|
|
958
|
+
#: than asserting something about a file that does not exist. The closing
|
|
959
|
+
#: sentence differs on purpose: pre-build EVERYTHING is still a placeholder,
|
|
960
|
+
#: so "nothing else is missing" would be false here.
|
|
961
|
+
OPENOCD_NO_HOSTTOOLS_NOTE_PRE_BUILD = (
|
|
962
|
+
"No OpenOCD executable could be resolved for this draft, so it carries no "
|
|
963
|
+
"'serverpath' (and no 'searchDir'): this project has no build output for "
|
|
964
|
+
"tan to read a runners.yaml out of yet, and no 'openocd' is on PATH here "
|
|
965
|
+
"either. "
|
|
966
|
+
+ _OPENOCD_ZEPHYR_ADVICE
|
|
967
|
+
+ " The rest of this draft is unresolved for the ordinary pre-build "
|
|
968
|
+
"reason the placeholder note above gives, not for want of host tools."
|
|
969
|
+
)
|
|
970
|
+
|
|
971
|
+
#: `baremetal-mcu`: no Zephyr, so no Zephyr-SDK paragraph. The actionable half
|
|
972
|
+
#: only, plus the real reason `serverpath` is absent on this target kind.
|
|
973
|
+
OPENOCD_NO_SERVERPATH_NOTE_BAREMETAL = (
|
|
974
|
+
"No OpenOCD executable could be resolved for this draft, so it carries no "
|
|
975
|
+
"'serverpath' (and no 'searchDir'), and no 'openocd' is on PATH here "
|
|
976
|
+
"either. This is a baremetal target, built by tan's plain-CMake backend "
|
|
977
|
+
"rather than by Zephyr, and tan fills 'serverpath' in only from a Zephyr "
|
|
978
|
+
"build's runners.yaml -- so nothing here recorded an OpenOCD path and it "
|
|
979
|
+
"has to come from the system. Install it (your package manager's "
|
|
980
|
+
"'openocd', or a vendor build) and put it on PATH; that alone is enough "
|
|
981
|
+
"for a session, because cortex-debug falls back to its own PATH lookup "
|
|
982
|
+
"when 'serverpath' is absent."
|
|
983
|
+
)
|
|
984
|
+
|
|
985
|
+
|
|
986
|
+
def _openocd_hosttools_note(
|
|
987
|
+
draft: dict[str, Any], server: str, registered_runners: list[str], target: str
|
|
988
|
+
) -> str | None:
|
|
989
|
+
"""The OpenOCD-without-a-`serverpath` note for the state this run is
|
|
990
|
+
actually in, or `None` when OpenOCD was not asked for, the resolution
|
|
991
|
+
produced a `serverpath`, or an `openocd` is on PATH. Which of the three
|
|
992
|
+
spellings, and why the wording is split at all, is in the block comment
|
|
993
|
+
above."""
|
|
994
|
+
if server != OPENOCD or draft.get("servertype") != OPENOCD:
|
|
995
|
+
return None
|
|
996
|
+
if "serverpath" in draft:
|
|
997
|
+
return None
|
|
998
|
+
if _first_on_path(_RUNTIME_EXECUTABLES[OPENOCD]) is not None:
|
|
999
|
+
return None
|
|
1000
|
+
if target == BAREMETAL_MCU:
|
|
1001
|
+
return OPENOCD_NO_SERVERPATH_NOTE_BAREMETAL
|
|
1002
|
+
if not registered_runners:
|
|
1003
|
+
return OPENOCD_NO_HOSTTOOLS_NOTE_PRE_BUILD
|
|
1004
|
+
return OPENOCD_NO_HOSTTOOLS_NOTE
|
|
1005
|
+
|
|
1006
|
+
|
|
1007
|
+
def _preview_notes_for(
|
|
1008
|
+
draft: dict[str, Any], registered_runners: list[str], server: str, target: str
|
|
1009
|
+
) -> list[str]:
|
|
1010
|
+
"""The preview notes, minus the "still needs resolution" warning once nothing
|
|
1011
|
+
is left to resolve. Keyed off the FINAL draft rather than off "did anything
|
|
1012
|
+
resolve": a partly-resolved config (a board that registers no OpenOCD runner
|
|
1013
|
+
still has `<resolved-openocd-board-cfg>`) must keep the warning, and a fully
|
|
1014
|
+
resolved one must lose it -- otherwise the note is noise on configs that are
|
|
1015
|
+
fine and silence on configs that are not.
|
|
1016
|
+
"""
|
|
1017
|
+
notes = [
|
|
1018
|
+
n
|
|
1019
|
+
for n in launch_preview_notes()
|
|
1020
|
+
if not n.startswith("Placeholder fields") or _has_placeholder(draft)
|
|
1021
|
+
]
|
|
1022
|
+
# The most common reason a placeholder survives: the board never registered
|
|
1023
|
+
# this server. Say so, instead of leaving the user to wonder which
|
|
1024
|
+
# project-specific value they are supposed to invent.
|
|
1025
|
+
runner = _RUNNER_ID.get(server)
|
|
1026
|
+
if runner is not None and registered_runners and runner not in registered_runners:
|
|
1027
|
+
# `{:?}` on a Vec<String> renders `["jlink", "openocd"]` -- reproduced
|
|
1028
|
+
# exactly, because this string is the note a customer reads and diffs
|
|
1029
|
+
# against the Rust binary's own output.
|
|
1030
|
+
rendered = "[" + ", ".join(f'"{r}"' for r in registered_runners) + "]"
|
|
1031
|
+
notes.append(
|
|
1032
|
+
f"This build registers no '{runner}' runner (runners.yaml: {rendered}), "
|
|
1033
|
+
"so its fields could not be resolved."
|
|
1034
|
+
)
|
|
1035
|
+
hosttools_note = _openocd_hosttools_note(draft, server, registered_runners, target)
|
|
1036
|
+
if hosttools_note is not None:
|
|
1037
|
+
notes.append(hosttools_note)
|
|
1038
|
+
return notes
|
|
1039
|
+
|
|
1040
|
+
|
|
1041
|
+
def _migrated_issue(from_name: str, to_name: str) -> Issue:
|
|
1042
|
+
"""The #133 migration report: a pre-#155 `"ALP: ..."` entry was found in
|
|
1043
|
+
place of the current `"Alp: ..."` name and adopted onto it. Severity `info`,
|
|
1044
|
+
not `warning` or `error` -- nothing failed and no action is required; this
|
|
1045
|
+
exists so a consumer (or a customer reading `--format json`) can tell WHY the
|
|
1046
|
+
file changed under them instead of only by diffing it."""
|
|
1047
|
+
return Issue(
|
|
1048
|
+
"debug-config.legacy-entry-migrated",
|
|
1049
|
+
"info",
|
|
1050
|
+
f'Migrated the legacy launch-configuration entry "{from_name}" into '
|
|
1051
|
+
f'"{to_name}". Any value you had hand-filled in on the old entry for an '
|
|
1052
|
+
"unresolved-placeholder field (device, miDebuggerServerAddress, "
|
|
1053
|
+
"configFiles, …) carried across; every other field tan owns was "
|
|
1054
|
+
"refreshed to this run's values, same as an ordinary re-run. The old "
|
|
1055
|
+
"entry is gone.",
|
|
1056
|
+
)
|
|
1057
|
+
|
|
1058
|
+
|
|
1059
|
+
def _legacy_untouched_issue(legacy_name: str) -> Issue:
|
|
1060
|
+
"""tan-cli#179: the ORDINARY same-name merge ran and a legacy `"ALP: ..."`
|
|
1061
|
+
counterpart of the SAME draft ALSO still sits in the file. Distinct from
|
|
1062
|
+
[`_migrated_issue`], which fires on the MISS path where the legacy entry is
|
|
1063
|
+
the one adopted -- here NEITHER entry was touched beyond the ordinary merge,
|
|
1064
|
+
so the customer's real hand-filled values may still be stranded on the
|
|
1065
|
+
leftover entry with nothing pointing at it."""
|
|
1066
|
+
return Issue(
|
|
1067
|
+
"debug-config.legacy-entry-untouched",
|
|
1068
|
+
"info",
|
|
1069
|
+
f'A leftover legacy launch-configuration entry "{legacy_name}" still '
|
|
1070
|
+
"sits in .vscode/launch.json alongside the entry this run updated. It "
|
|
1071
|
+
"was left untouched — nothing decides which of the two you may have "
|
|
1072
|
+
"hand-edited is authoritative — so if you filled in real values on "
|
|
1073
|
+
"the legacy entry, copy them onto the maintained one and remove the "
|
|
1074
|
+
"legacy entry yourself.",
|
|
1075
|
+
)
|
|
1076
|
+
|
|
1077
|
+
|
|
1078
|
+
def _sdk_identity_key_absent_issue(field: str) -> Issue:
|
|
1079
|
+
"""alp-sdk#1026 review finding #4: emitted when the SDK DID publish a
|
|
1080
|
+
debug-probe identity for this project's SoC variant, but that identity
|
|
1081
|
+
does not (yet) include a value for `field` -- distinct from, and more
|
|
1082
|
+
specific than, the generic "Placeholder fields..." note every unresolved
|
|
1083
|
+
field already gets regardless of why. Severity `info`: this is the
|
|
1084
|
+
schema's own documented stance that an unpopulated key is a published
|
|
1085
|
+
"unknown", not an error and not a bug."""
|
|
1086
|
+
return Issue(
|
|
1087
|
+
"debug-config.sdk-identity-key-absent",
|
|
1088
|
+
"info",
|
|
1089
|
+
f"This SoM's SDK-published debug-probe identity (alp-sdk#987) does not "
|
|
1090
|
+
f"include a value for `{field}` yet, so it stays the placeholder shown "
|
|
1091
|
+
"in `configuration` — an unpopulated key is the correct published "
|
|
1092
|
+
'"unknown" (alp-sdk#1026), never a guess.',
|
|
1093
|
+
)
|
|
1094
|
+
|
|
1095
|
+
|
|
1096
|
+
def _sdk_identity_core_unresolved_issue(core: str | None, known_cores: frozenset[str]) -> Issue:
|
|
1097
|
+
"""tan-cli#489 (4): distinct from [`_sdk_identity_key_absent_issue`] above,
|
|
1098
|
+
which is only correct when the field genuinely has no core dependency
|
|
1099
|
+
(`configFiles`/`targetId`) or a core WAS resolved and is simply not the
|
|
1100
|
+
one the SDK's map happens to key `device` under. On a never-built project
|
|
1101
|
+
with no `--core`, `jlink_device.get(None)` is structurally always `None`
|
|
1102
|
+
-- the SDK's map is never even consulted -- so `sdk-identity-key-absent`
|
|
1103
|
+
told the customer "the SDK publishes no `device` for this SoM" when
|
|
1104
|
+
`metadata/socs/.../*.json` may publish one for every core it has. Both
|
|
1105
|
+
"no core id was resolved" and "the given core id has no entry in the
|
|
1106
|
+
published map" share the SAME working remedy (pass `--core <id>` naming a
|
|
1107
|
+
core the SDK DOES publish), so this one code covers both; the message
|
|
1108
|
+
differs only in whether a core was named to look up."""
|
|
1109
|
+
if core is None:
|
|
1110
|
+
return Issue(
|
|
1111
|
+
"debug-config.sdk-identity-core-unresolved",
|
|
1112
|
+
"info",
|
|
1113
|
+
"This SoM's SDK-published debug-probe identity (alp-sdk#987) is "
|
|
1114
|
+
"keyed per core, and no core id was resolved for this run (no "
|
|
1115
|
+
"--core given, and this project has no prior build to infer one "
|
|
1116
|
+
"from) — so `device` stays the placeholder shown in "
|
|
1117
|
+
"`configuration`, even though the SDK may publish a value for "
|
|
1118
|
+
"one or more of this SoM's cores. Pass --core <id> to resolve it.",
|
|
1119
|
+
)
|
|
1120
|
+
cores = ", ".join(sorted(known_cores)) if known_cores else "none"
|
|
1121
|
+
return Issue(
|
|
1122
|
+
"debug-config.sdk-identity-core-unresolved",
|
|
1123
|
+
"info",
|
|
1124
|
+
f"This SoM's SDK-published debug-probe identity (alp-sdk#987) has no "
|
|
1125
|
+
f"`device` entry for core '{core}' — its published cores are: "
|
|
1126
|
+
f"{cores}. `device` stays the placeholder shown in `configuration`; "
|
|
1127
|
+
"pass --core with one of the cores above, if that is the core you "
|
|
1128
|
+
"meant.",
|
|
1129
|
+
)
|
|
1130
|
+
|
|
1131
|
+
|
|
1132
|
+
def _sdk_identity_overwrite_issue(field: str, existing_value: str, incoming_value: str) -> Issue:
|
|
1133
|
+
"""alp-sdk#1026 review finding #1: emitted whenever the SDK's published
|
|
1134
|
+
debug-probe identity (not a real build) just replaced a concrete existing
|
|
1135
|
+
value on the entry this run wrote. Severity `info`, same reasoning as its
|
|
1136
|
+
three siblings above: the overwrite itself is not new or wrong (a value
|
|
1137
|
+
resolved from a real build already overwrote unconditionally, by design --
|
|
1138
|
+
see `tan.core.debug_launch._merge_configuration`'s doc comment) but a tool
|
|
1139
|
+
that replaces a customer's own value at `exit 0` with `issues: []` has
|
|
1140
|
+
told them nothing happened."""
|
|
1141
|
+
return Issue(
|
|
1142
|
+
"debug-config.sdk-identity-overwrite",
|
|
1143
|
+
"info",
|
|
1144
|
+
f'This write replaced the existing `{field}` value "{existing_value}" with '
|
|
1145
|
+
f'"{incoming_value}", resolved from the SDK\'s published debug-probe identity '
|
|
1146
|
+
"(alp-sdk#987) rather than from a real build. If "
|
|
1147
|
+
f'"{existing_value}" was a value you filled in on purpose — e.g. a J-Link '
|
|
1148
|
+
"flash-unlock device profile more specific than the generic attach device "
|
|
1149
|
+
"the SDK publishes — restore it in .vscode/launch.json; a value tan itself "
|
|
1150
|
+
"resolves from a real build will overwrite it again the same way.",
|
|
1151
|
+
)
|
|
1152
|
+
|
|
1153
|
+
|
|
1154
|
+
def _sdk_identity_appended_issue(field: str, existing_value: str, incoming_value: str) -> Issue:
|
|
1155
|
+
"""tan-cli#982 review finding #2: the accepted degradation
|
|
1156
|
+
[`sdk_identity_stranded_appends`] exists to name -- an existing `field`
|
|
1157
|
+
entry `provenance` could not prove was tan's own prior output was left in
|
|
1158
|
+
place, and the value resolved this run from the SDK's published
|
|
1159
|
+
debug-probe identity (alp-sdk#987) was APPENDED beside it rather than
|
|
1160
|
+
replacing it. Severity `info`, same family as
|
|
1161
|
+
`debug-config.comments-dropped` / `debug-config.legacy-entry-untouched`:
|
|
1162
|
+
nothing failed, but a customer never told two `configFiles` entries now
|
|
1163
|
+
sit on the same launch configuration has no way to know one is stale --
|
|
1164
|
+
OpenOCD sources every `-f`, so two board configs on one TAP fail the
|
|
1165
|
+
debug session outright, the same failure class this write just created
|
|
1166
|
+
silently."""
|
|
1167
|
+
return Issue(
|
|
1168
|
+
"debug-config.sdk-identity-appended",
|
|
1169
|
+
"info",
|
|
1170
|
+
f'This write left the existing `{field}` value "{existing_value}" in place '
|
|
1171
|
+
f'and appended "{incoming_value}", resolved from the SDK\'s published '
|
|
1172
|
+
"debug-probe identity (alp-sdk#987), instead of replacing it -- tan could "
|
|
1173
|
+
f'not prove "{existing_value}" was its own prior output (no recorded '
|
|
1174
|
+
"`.alp/` provenance for it), so it left it rather than risk overwriting a "
|
|
1175
|
+
"value you filled in by hand. If it is stale, delete it from "
|
|
1176
|
+
".vscode/launch.json yourself.",
|
|
1177
|
+
)
|
|
1178
|
+
|
|
1179
|
+
|
|
1180
|
+
def _load_files_preserved_issue(existing_value: str, incoming_value: str) -> Issue:
|
|
1181
|
+
"""tan-cli#1020 review: the ``loadFiles`` sibling of
|
|
1182
|
+
``debug-config.sdk-identity-appended`` above -- ``loadFiles`` names ONE
|
|
1183
|
+
deliberate artefact list per configuration, not a set of independently
|
|
1184
|
+
owned entries, so its own merge rule (``debug_launch._merge_load_files``)
|
|
1185
|
+
never appends beside an unproven existing value the way
|
|
1186
|
+
`configFiles`/`setupCommands` do; it leaves the existing value untouched,
|
|
1187
|
+
wholesale. Severity ``info``, same family as its siblings: nothing
|
|
1188
|
+
failed, but a tool that silently keeps a customer's value at `exit 0`
|
|
1189
|
+
with `issues: []` -- including a customer's own deliberate attach-only
|
|
1190
|
+
``[]`` -- has told them nothing about the divergence between what is on
|
|
1191
|
+
disk and what this run would otherwise have written."""
|
|
1192
|
+
return Issue(
|
|
1193
|
+
"debug-config.load-files-preserved",
|
|
1194
|
+
"info",
|
|
1195
|
+
f'This write left the existing `loadFiles` value "{existing_value}" in place '
|
|
1196
|
+
f'instead of replacing it with "{incoming_value}", resolved from this run\'s '
|
|
1197
|
+
"own draft -- tan could not prove the existing value was its own prior output "
|
|
1198
|
+
"(no recorded `.alp/` provenance for it), so it left it rather than risk "
|
|
1199
|
+
"overwriting a value that may have been filled in by hand, including an "
|
|
1200
|
+
"explicit `[]` for an attach-only session. If it is stale, update it in "
|
|
1201
|
+
".vscode/launch.json yourself.",
|
|
1202
|
+
)
|
|
1203
|
+
|
|
1204
|
+
|
|
1205
|
+
def _comments_dropped_issue() -> Issue:
|
|
1206
|
+
"""tan-cli#182 review finding #2: this write dropped a comment (or trailing
|
|
1207
|
+
comma) sitting inside a span it rewrote. Severity `info` -- nothing failed
|
|
1208
|
+
and there is no action to take, but a tool that discarded user-authored
|
|
1209
|
+
content must never report unqualified success (#182's own floor)."""
|
|
1210
|
+
return Issue(
|
|
1211
|
+
"debug-config.comments-dropped",
|
|
1212
|
+
"info",
|
|
1213
|
+
"This write dropped a comment (or trailing comma) that sat inside the "
|
|
1214
|
+
"part of .vscode/launch.json it rewrote — either inside the one entry "
|
|
1215
|
+
"being updated, or, if the file's shape couldn't be confidently "
|
|
1216
|
+
"spliced, anywhere in the file. Everything outside that span is "
|
|
1217
|
+
"untouched.",
|
|
1218
|
+
)
|
|
1219
|
+
|
|
1220
|
+
|
|
1221
|
+
def _data(
|
|
1222
|
+
*,
|
|
1223
|
+
generated_at: str,
|
|
1224
|
+
target: str,
|
|
1225
|
+
server: str,
|
|
1226
|
+
preview: bool,
|
|
1227
|
+
launch_json_path: str,
|
|
1228
|
+
replaced: bool,
|
|
1229
|
+
notes: list[str],
|
|
1230
|
+
configuration: Any,
|
|
1231
|
+
) -> dict[str, Any]:
|
|
1232
|
+
return {
|
|
1233
|
+
"schemaVersion": DATA_SCHEMA_VERSION,
|
|
1234
|
+
"generatedAt": generated_at,
|
|
1235
|
+
"targetKind": target,
|
|
1236
|
+
"server": server,
|
|
1237
|
+
"preview": preview,
|
|
1238
|
+
"launchJsonPath": launch_json_path,
|
|
1239
|
+
"replaced": replaced,
|
|
1240
|
+
# tan-cli#945: producer-stated, so a consumer never has to re-derive
|
|
1241
|
+
# "does starting this profile write real hardware" from cortex-debug's
|
|
1242
|
+
# own adapter schema (its `loadFiles` default, or one of thirteen
|
|
1243
|
+
# `*Commands` lists that could carry a bare `load`) the way
|
|
1244
|
+
# alp-sdk-vscode#586 had to before its consent dialog could even ask
|
|
1245
|
+
# the question. Derived from `target` alone (`programs_device`), so it
|
|
1246
|
+
# is present on every outcome this command can report, including a
|
|
1247
|
+
# failure before a `configuration` was ever built -- but on one of
|
|
1248
|
+
# the internal-failure backstop paths (`_internal_failure` and its
|
|
1249
|
+
# siblings), `target` is a fixed `zephyr-mcu`/`none` placeholder that
|
|
1250
|
+
# never learned what was actually asked for (tan-cli#1020 review),
|
|
1251
|
+
# so there `programsDevice` is present and CONSERVATIVE (`true`,
|
|
1252
|
+
# fail-safe), not necessarily an accurate answer for the target the
|
|
1253
|
+
# caller actually named.
|
|
1254
|
+
"programsDevice": programs_device(target),
|
|
1255
|
+
"notes": notes,
|
|
1256
|
+
# The launch configuration itself -- the very thing the command
|
|
1257
|
+
# produces. Additive: the envelope used to describe the write (path,
|
|
1258
|
+
# replaced, notes) without carrying the object, so an automated consumer
|
|
1259
|
+
# had to re-read launch.json or scrape the text preview to see what was
|
|
1260
|
+
# generated (alp-sdk-vscode#339).
|
|
1261
|
+
"configuration": configuration,
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
|
|
1265
|
+
def _failure(
|
|
1266
|
+
*,
|
|
1267
|
+
generated_at: str,
|
|
1268
|
+
target: str,
|
|
1269
|
+
server: str,
|
|
1270
|
+
launch_json_path: str,
|
|
1271
|
+
exit_code: ExitCode,
|
|
1272
|
+
code: str,
|
|
1273
|
+
message: str,
|
|
1274
|
+
text_lines: list[str],
|
|
1275
|
+
) -> _Outcome:
|
|
1276
|
+
"""The shared failure outcome: one `error` issue, a `configuration: null`
|
|
1277
|
+
payload, and a NULL project (TS `createFailureResult` reports no project)."""
|
|
1278
|
+
return _Outcome(
|
|
1279
|
+
exit_code=exit_code,
|
|
1280
|
+
data=_data(
|
|
1281
|
+
generated_at=generated_at,
|
|
1282
|
+
target=target,
|
|
1283
|
+
server=server,
|
|
1284
|
+
preview=False,
|
|
1285
|
+
launch_json_path=launch_json_path,
|
|
1286
|
+
replaced=False,
|
|
1287
|
+
notes=[],
|
|
1288
|
+
# No draft exists on this path -- the failure happened before (or
|
|
1289
|
+
# instead of) generating one. `null`, not an empty object, so a
|
|
1290
|
+
# consumer cannot mistake it for a configuration with no fields.
|
|
1291
|
+
configuration=None,
|
|
1292
|
+
),
|
|
1293
|
+
project=Project(root=None, board_yaml=None),
|
|
1294
|
+
issues=[Issue(f"debug-config.{code}", "error", message)],
|
|
1295
|
+
text=[*text_lines, message],
|
|
1296
|
+
)
|
|
1297
|
+
|
|
1298
|
+
|
|
1299
|
+
def _internal_failure(
|
|
1300
|
+
generated_at: str, message: str, launch_json_path: str
|
|
1301
|
+
) -> _Outcome:
|
|
1302
|
+
"""Invalid kind / unsupported backend / unreadable or malformed existing
|
|
1303
|
+
launch.json: exit 5, with a `zephyr-mcu`/`none` placeholder target (matching
|
|
1304
|
+
the TS catch block, which never learned what was actually asked for)."""
|
|
1305
|
+
return _failure(
|
|
1306
|
+
generated_at=generated_at,
|
|
1307
|
+
target=ZEPHYR_MCU,
|
|
1308
|
+
server=SERVER_NONE,
|
|
1309
|
+
launch_json_path=launch_json_path,
|
|
1310
|
+
exit_code=ExitCode.INTERNAL_FAILURE,
|
|
1311
|
+
code="internal-failure",
|
|
1312
|
+
message=message,
|
|
1313
|
+
text_lines=["debug-config: internal failure"],
|
|
1314
|
+
)
|
|
1315
|
+
|
|
1316
|
+
|
|
1317
|
+
def _build_manifest_missing_failure(
|
|
1318
|
+
generated_at: str, message: str, launch_json_path: str
|
|
1319
|
+
) -> _Outcome:
|
|
1320
|
+
"""tan-cli#462: a real hardware project (`som.sku` set) has not been built
|
|
1321
|
+
yet, so `--target-kind` cannot be inferred from a `build/system-
|
|
1322
|
+
manifest.yaml` that does not exist -- a precondition the CALLER can fix
|
|
1323
|
+
(`tan build` first, or an explicit `--target-kind`), not a tan-side
|
|
1324
|
+
crash. Exit 2, matching the sibling distinction tan-cli#262 settled for
|
|
1325
|
+
`tan validate`: a verdict the command CAN produce about the caller's own
|
|
1326
|
+
input is `VALIDATION_FAILURE`, never `INTERNAL_FAILURE` -- that exit
|
|
1327
|
+
stays reserved for the `except Exception` backstop below and an
|
|
1328
|
+
unreadable/malformed existing launch.json."""
|
|
1329
|
+
return _failure(
|
|
1330
|
+
generated_at=generated_at,
|
|
1331
|
+
target=ZEPHYR_MCU,
|
|
1332
|
+
server=SERVER_NONE,
|
|
1333
|
+
launch_json_path=launch_json_path,
|
|
1334
|
+
exit_code=ExitCode.VALIDATION_FAILURE,
|
|
1335
|
+
code="build-manifest-missing",
|
|
1336
|
+
message=message,
|
|
1337
|
+
text_lines=["debug-config: validation failure"],
|
|
1338
|
+
)
|
|
1339
|
+
|
|
1340
|
+
|
|
1341
|
+
def _project_not_found_failure(
|
|
1342
|
+
generated_at: str, message: str, launch_json_path: str
|
|
1343
|
+
) -> _Outcome:
|
|
1344
|
+
"""tan-cli#476: `--project` names a directory that does not exist.
|
|
1345
|
+
|
|
1346
|
+
Refused rather than created. `_write_launch_json` calls `mkdir(parents=
|
|
1347
|
+
True)`, so a typo'd `--project` (or a stale path in a script) silently
|
|
1348
|
+
MATERIALISED a project tree and dropped a `launch.json` in it, at exit 0
|
|
1349
|
+
with `issues: []`. Nothing downstream could tell that apart from writing
|
|
1350
|
+
into a real project.
|
|
1351
|
+
|
|
1352
|
+
A deliberate divergence from the oracle, which does the same thing --
|
|
1353
|
+
measured, not assumed: `target/release/tan debug-config --project <ghost>`
|
|
1354
|
+
exits 0 and leaves `<ghost>/.vscode/launch.json` behind. No parity CASE
|
|
1355
|
+
pins it (all four frozen `debug-config` argvs run in an existing
|
|
1356
|
+
`work_dir`), so this changes no frozen comparison. `--project` names a
|
|
1357
|
+
project that EXISTS; a path that does not is the caller's own input error,
|
|
1358
|
+
hence `ValidationFailure` (2), the same class as tan-cli#462's four.
|
|
1359
|
+
|
|
1360
|
+
A separate code from `_invalid_argument_failure` below, on purpose
|
|
1361
|
+
(tan-cli#508 review): this fires before target/server are even parsed --
|
|
1362
|
+
a precondition on the WORKSPACE, the same class as
|
|
1363
|
+
`_build_manifest_missing_failure` above, not a flag-value shape check."""
|
|
1364
|
+
return _failure(
|
|
1365
|
+
generated_at=generated_at,
|
|
1366
|
+
target=ZEPHYR_MCU,
|
|
1367
|
+
server=SERVER_NONE,
|
|
1368
|
+
launch_json_path=launch_json_path,
|
|
1369
|
+
exit_code=ExitCode.VALIDATION_FAILURE,
|
|
1370
|
+
code="project-not-found",
|
|
1371
|
+
message=message,
|
|
1372
|
+
text_lines=["debug-config: validation failure"],
|
|
1373
|
+
)
|
|
1374
|
+
|
|
1375
|
+
|
|
1376
|
+
def _target_kind_unresolved_failure(
|
|
1377
|
+
generated_at: str, message: str, launch_json_path: str
|
|
1378
|
+
) -> _Outcome:
|
|
1379
|
+
"""tan-cli#476 half (b): `--target-kind` was omitted and this project
|
|
1380
|
+
offers NO signal to infer one from -- no `build/system-manifest.yaml`, and
|
|
1381
|
+
no `board.yaml` declaring a `som.sku`. Refused rather than silently
|
|
1382
|
+
defaulting to `native-host`.
|
|
1383
|
+
|
|
1384
|
+
The other half of the same report. Half (a) -- a `--project` that does not
|
|
1385
|
+
exist -- is [`_project_not_found_failure`] above, fixed in tan-cli#508;
|
|
1386
|
+
that PR's own body records this half as "deliberately left". The two share
|
|
1387
|
+
one symptom: a `native_sim` launch configuration materialising, at exit 0
|
|
1388
|
+
with `issues: []`, in a directory that is not a project. #508 stopped the
|
|
1389
|
+
directory being CREATED; a directory that already exists (the "ran it from
|
|
1390
|
+
the wrong cwd" case the issue names in its own words) still got the file.
|
|
1391
|
+
|
|
1392
|
+
`parse_target_kind(None)` -> `NATIVE_HOST` is a REAL default for a
|
|
1393
|
+
project that says so -- `infer_target_kind` returns `NATIVE_HOST` outright
|
|
1394
|
+
when every slice a build produced is native_sim. This refusal fires only
|
|
1395
|
+
on the no-evidence case that default was standing in for, so a project
|
|
1396
|
+
with any signal at all is untouched, and `--target-kind native-host`
|
|
1397
|
+
remains the way to ask for exactly this draft on purpose.
|
|
1398
|
+
|
|
1399
|
+
Exit 2, the same class as `_build_manifest_missing_failure` above and for
|
|
1400
|
+
the same reason: a precondition on the WORKSPACE that the caller can fix,
|
|
1401
|
+
never a tan-side crash. A deliberate divergence from the Rust oracle,
|
|
1402
|
+
which exits 0 with the native-host draft -- measured, not inferred
|
|
1403
|
+
(`target/debug/tan --format json debug-config --project <empty> --preview`
|
|
1404
|
+
-> exit 0, `targetKind: "native-host"`). No parity CASE and no frozen
|
|
1405
|
+
conformance golden pins it: all five frozen `debug-config` argvs pass
|
|
1406
|
+
`--target-kind` explicitly, so none of them reaches the inference path at
|
|
1407
|
+
all."""
|
|
1408
|
+
return _failure(
|
|
1409
|
+
generated_at=generated_at,
|
|
1410
|
+
target=ZEPHYR_MCU,
|
|
1411
|
+
server=SERVER_NONE,
|
|
1412
|
+
launch_json_path=launch_json_path,
|
|
1413
|
+
exit_code=ExitCode.VALIDATION_FAILURE,
|
|
1414
|
+
code="target-kind-unresolved",
|
|
1415
|
+
message=message,
|
|
1416
|
+
text_lines=["debug-config: validation failure"],
|
|
1417
|
+
)
|
|
1418
|
+
|
|
1419
|
+
|
|
1420
|
+
def _invalid_argument_failure(
|
|
1421
|
+
generated_at: str,
|
|
1422
|
+
message: str,
|
|
1423
|
+
launch_json_path: str,
|
|
1424
|
+
target: str = ZEPHYR_MCU,
|
|
1425
|
+
server: str = SERVER_NONE,
|
|
1426
|
+
) -> _Outcome:
|
|
1427
|
+
"""tan-cli#477: a flag VALUE outside the set this command accepts.
|
|
1428
|
+
|
|
1429
|
+
Covers `--target-kind`, `--server`, an unsupported target+server pairing,
|
|
1430
|
+
`--svd` and `--gdbserver-address` -- every `DebugConfigError` raised while
|
|
1431
|
+
turning the caller's own arguments into a draft. All five already produced
|
|
1432
|
+
a complete, actionable message; only the verdict was wrong:
|
|
1433
|
+
|
|
1434
|
+
Unsupported --target-kind 'bogus'. Allowed values: zephyr-mcu,
|
|
1435
|
+
baremetal-mcu, yocto-userspace, native-host.
|
|
1436
|
+
|
|
1437
|
+
Reporting that as `internal failure` / exit 5 tells the user tan crashed
|
|
1438
|
+
and tells CI to treat a typo as a tool defect. tan-cli#462 made exactly
|
|
1439
|
+
this argument for the four PRECONDITIONS and reclassified them; the
|
|
1440
|
+
argument-validation half was not part of that change, so `--core bogus` --
|
|
1441
|
+
quoted verbatim in #462's own body -- still exited 5 in v0.5.1.
|
|
1442
|
+
|
|
1443
|
+
Exit 5 stays reserved for what `_build_manifest_missing_failure`'s
|
|
1444
|
+
docstring already names: the `except Exception` backstop, and an
|
|
1445
|
+
unreadable or malformed EXISTING launch.json. Neither is reachable from a
|
|
1446
|
+
flag value.
|
|
1447
|
+
|
|
1448
|
+
A DELIBERATE DIVERGENCE from the retired Rust oracle, which exited 5
|
|
1449
|
+
here -- measured (`target/release/tan debug-config --target-kind bogus` ->
|
|
1450
|
+
5) while both implementations existed. `test_oracle_parity.py` pinned it
|
|
1451
|
+
("Pins exit 5 ... across both implementations") until tan-cli#269 deleted
|
|
1452
|
+
that module with `crates/`, so there is no CASE table left to update in the
|
|
1453
|
+
same commit. The line is pinned tan-side instead, on both sides of it:
|
|
1454
|
+
`tests/commands/test_debug_config_command.py`'s
|
|
1455
|
+
`test_a_refused_selector_is_a_coded_envelope_at_exit_2` holds the 2, and
|
|
1456
|
+
`test_a_malformed_existing_launch_json_stays_an_internal_failure` holds the
|
|
1457
|
+
reserved 5."""
|
|
1458
|
+
return _failure(
|
|
1459
|
+
generated_at=generated_at,
|
|
1460
|
+
target=target,
|
|
1461
|
+
server=server,
|
|
1462
|
+
launch_json_path=launch_json_path,
|
|
1463
|
+
exit_code=ExitCode.VALIDATION_FAILURE,
|
|
1464
|
+
code="invalid-argument",
|
|
1465
|
+
message=message,
|
|
1466
|
+
text_lines=["debug-config: validation failure"],
|
|
1467
|
+
)
|
|
1468
|
+
|
|
1469
|
+
|
|
1470
|
+
def _core_unknown_failure(generated_at: str, message: str, launch_json_path: str) -> _Outcome:
|
|
1471
|
+
"""tan-cli#462: an explicit `--core` (with `--target-kind` omitted) names
|
|
1472
|
+
no slice this project's own build produced -- the caller's own typo or a
|
|
1473
|
+
stale flag, not a tan-side crash. Exit 2, same reasoning as
|
|
1474
|
+
[`_build_manifest_missing_failure`] above."""
|
|
1475
|
+
return _failure(
|
|
1476
|
+
generated_at=generated_at,
|
|
1477
|
+
target=ZEPHYR_MCU,
|
|
1478
|
+
server=SERVER_NONE,
|
|
1479
|
+
launch_json_path=launch_json_path,
|
|
1480
|
+
exit_code=ExitCode.VALIDATION_FAILURE,
|
|
1481
|
+
code="core-unknown",
|
|
1482
|
+
message=message,
|
|
1483
|
+
text_lines=["debug-config: validation failure"],
|
|
1484
|
+
)
|
|
1485
|
+
|
|
1486
|
+
|
|
1487
|
+
def _explicit_core_unknown_failure(
|
|
1488
|
+
generated_at: str, target: str, server: str, message: str, launch_json_path: str
|
|
1489
|
+
) -> _Outcome:
|
|
1490
|
+
"""tan-cli#489 (5): the SAME `core-unknown` refusal as
|
|
1491
|
+
[`_core_unknown_failure`] above, reached from the OTHER side --
|
|
1492
|
+
`--target-kind` given EXPLICITLY, so `infer_target_kind` (and its own
|
|
1493
|
+
`--core`-vs-manifest guard) never runs at all, and an explicit `--core`
|
|
1494
|
+
naming no slice in this project's own build used to sail through in
|
|
1495
|
+
silence: exit 0, a placeholder `device`, and an `executable` pointing at
|
|
1496
|
+
a path that does not exist in this project. Same code, same exit, same
|
|
1497
|
+
caller-fixable cause -- only the TARGET/SERVER differ: both are already
|
|
1498
|
+
resolved here (unlike the omitted-`--target-kind` path, which fires
|
|
1499
|
+
before either is known), so this reports the REAL ones instead of
|
|
1500
|
+
`_core_unknown_failure`'s `zephyr-mcu`/`none` placeholder pair."""
|
|
1501
|
+
return _failure(
|
|
1502
|
+
generated_at=generated_at,
|
|
1503
|
+
target=target,
|
|
1504
|
+
server=server,
|
|
1505
|
+
launch_json_path=launch_json_path,
|
|
1506
|
+
exit_code=ExitCode.VALIDATION_FAILURE,
|
|
1507
|
+
code="core-unknown",
|
|
1508
|
+
message=message,
|
|
1509
|
+
text_lines=["debug-config: validation failure"],
|
|
1510
|
+
)
|
|
1511
|
+
|
|
1512
|
+
|
|
1513
|
+
def _target_kind_ambiguous_failure(
|
|
1514
|
+
generated_at: str, message: str, launch_json_path: str
|
|
1515
|
+
) -> _Outcome:
|
|
1516
|
+
"""tan-cli#462 review round: `--target-kind` omitted, and this project's
|
|
1517
|
+
own `build/system-manifest.yaml` -- well-formed, fully built, tan's own
|
|
1518
|
+
output -- names more than one target class (a mixed-core board, e.g.
|
|
1519
|
+
E1M-V2N101/V2M101's `a55_cluster`+`m33_sm`) with no `--core` to narrow
|
|
1520
|
+
them. Worse than the two refusals above: it hits every run against a
|
|
1521
|
+
CORRECT project forever, not just a pre-build one, and the remedy the
|
|
1522
|
+
message itself names (`--target-kind` + `--core`) works. Still the
|
|
1523
|
+
caller's own precondition, not a tan crash -- exit 2, same reasoning as
|
|
1524
|
+
[`_build_manifest_missing_failure`] above."""
|
|
1525
|
+
return _failure(
|
|
1526
|
+
generated_at=generated_at,
|
|
1527
|
+
target=ZEPHYR_MCU,
|
|
1528
|
+
server=SERVER_NONE,
|
|
1529
|
+
launch_json_path=launch_json_path,
|
|
1530
|
+
exit_code=ExitCode.VALIDATION_FAILURE,
|
|
1531
|
+
code="target-kind-ambiguous",
|
|
1532
|
+
message=message,
|
|
1533
|
+
text_lines=["debug-config: validation failure"],
|
|
1534
|
+
)
|
|
1535
|
+
|
|
1536
|
+
|
|
1537
|
+
def _core_required_failure(
|
|
1538
|
+
generated_at: str, target: str, server: str, message: str, launch_json_path: str
|
|
1539
|
+
) -> _Outcome:
|
|
1540
|
+
"""tan-cli#1488: `--core` omitted on a build with several cores of the
|
|
1541
|
+
target class. The caller's own precondition (exit 2), like `tan probe` /
|
|
1542
|
+
`tan flash`, which refuse the same case."""
|
|
1543
|
+
return _failure(
|
|
1544
|
+
generated_at=generated_at,
|
|
1545
|
+
target=target,
|
|
1546
|
+
server=server,
|
|
1547
|
+
launch_json_path=launch_json_path,
|
|
1548
|
+
exit_code=ExitCode.VALIDATION_FAILURE,
|
|
1549
|
+
code="core-required",
|
|
1550
|
+
message=message,
|
|
1551
|
+
text_lines=["debug-config: validation failure"],
|
|
1552
|
+
)
|
|
1553
|
+
|
|
1554
|
+
|
|
1555
|
+
def _no_debuggable_target_class_failure(
|
|
1556
|
+
generated_at: str, message: str, launch_json_path: str
|
|
1557
|
+
) -> _Outcome:
|
|
1558
|
+
"""tan-cli#462 review round: `--target-kind` omitted, hardware slices
|
|
1559
|
+
exist, but none of their `os` values is one `MANIFEST_OS_BY_TARGET` maps
|
|
1560
|
+
(e.g. a lone `os: linux` slice) -- a knowledge/version skew between tan
|
|
1561
|
+
and the SDK, not a crash: no invariant was violated and the command still
|
|
1562
|
+
produced a coherent verdict with a working remedy (`--target-kind`
|
|
1563
|
+
explicit). Exit 2, same reasoning as [`_build_manifest_missing_failure`]
|
|
1564
|
+
above."""
|
|
1565
|
+
return _failure(
|
|
1566
|
+
generated_at=generated_at,
|
|
1567
|
+
target=ZEPHYR_MCU,
|
|
1568
|
+
server=SERVER_NONE,
|
|
1569
|
+
launch_json_path=launch_json_path,
|
|
1570
|
+
exit_code=ExitCode.VALIDATION_FAILURE,
|
|
1571
|
+
code="no-debuggable-target-class",
|
|
1572
|
+
message=message,
|
|
1573
|
+
text_lines=["debug-config: validation failure"],
|
|
1574
|
+
)
|
|
1575
|
+
|
|
1576
|
+
|
|
1577
|
+
def _write_failure(
|
|
1578
|
+
generated_at: str, target: str, server: str, launch_json_path: str, message: str
|
|
1579
|
+
) -> _Outcome:
|
|
1580
|
+
"""A filesystem error while creating `.vscode/` or writing launch.json: exit
|
|
1581
|
+
3, preserving the resolved target/server."""
|
|
1582
|
+
return _failure(
|
|
1583
|
+
generated_at=generated_at,
|
|
1584
|
+
target=target,
|
|
1585
|
+
server=server,
|
|
1586
|
+
launch_json_path=launch_json_path,
|
|
1587
|
+
exit_code=ExitCode.WRITE_FAILURE,
|
|
1588
|
+
code="write-failure",
|
|
1589
|
+
message=message,
|
|
1590
|
+
text_lines=["debug-config: failed to write launch.json."],
|
|
1591
|
+
)
|
|
1592
|
+
|
|
1593
|
+
|
|
1594
|
+
def _success_text(
|
|
1595
|
+
*,
|
|
1596
|
+
target: str,
|
|
1597
|
+
server: str,
|
|
1598
|
+
launch_json_path: str,
|
|
1599
|
+
replaced: bool,
|
|
1600
|
+
preview: bool,
|
|
1601
|
+
notes: list[str],
|
|
1602
|
+
configuration: Any,
|
|
1603
|
+
quiet: bool,
|
|
1604
|
+
issues: list[Issue],
|
|
1605
|
+
) -> list[str]:
|
|
1606
|
+
"""The human-readable lines for a successful preview or write."""
|
|
1607
|
+
lines: list[str] = []
|
|
1608
|
+
if preview:
|
|
1609
|
+
lines.append(f"debug-config: preview target={target} server={server}")
|
|
1610
|
+
lines.append(f"launch.json path: {launch_json_path}")
|
|
1611
|
+
if not quiet:
|
|
1612
|
+
lines.append("")
|
|
1613
|
+
lines.append(pretty_json(launch_preview_document(configuration)))
|
|
1614
|
+
lines.append("")
|
|
1615
|
+
lines.extend(f"note: {n}" for n in notes)
|
|
1616
|
+
return lines
|
|
1617
|
+
action = "updated" if replaced else "written"
|
|
1618
|
+
lines.append(f"debug-config: {action} target={target} server={server}")
|
|
1619
|
+
lines.append(f"launch.json: {launch_json_path}")
|
|
1620
|
+
# Always shown, even under --quiet: a one-time notice that the file just
|
|
1621
|
+
# lost a differently-named entry (folded into this one), or still holds a
|
|
1622
|
+
# stranded one, or lost a comment this run destroyed -- none of that is
|
|
1623
|
+
# routine noise like the resolution notes below it.
|
|
1624
|
+
for issue in issues:
|
|
1625
|
+
if issue.code in (
|
|
1626
|
+
"debug-config.legacy-entry-migrated",
|
|
1627
|
+
"debug-config.legacy-entry-untouched",
|
|
1628
|
+
):
|
|
1629
|
+
lines.append(f"debug-config: {issue.message}")
|
|
1630
|
+
for issue in issues:
|
|
1631
|
+
if issue.code == "debug-config.comments-dropped":
|
|
1632
|
+
lines.append(f"note: {issue.message}")
|
|
1633
|
+
# tan-cli#1020 round 4: `debug-config.load-files-preserved` is the
|
|
1634
|
+
# `flash-path`/`safety` disclosure this whole issue exists for -- a
|
|
1635
|
+
# customer at a terminal (the DEFAULT output mode, not `--format json`)
|
|
1636
|
+
# must be told `executable` and the actually-programmed `loadFiles` now
|
|
1637
|
+
# disagree, the same way `comments-dropped` already is. Emitting it only
|
|
1638
|
+
# under `--format json` left `launch_provenance.py`'s own "disclosed,
|
|
1639
|
+
# every run" claim true for a consumer that never reads text output and
|
|
1640
|
+
# false for one that does: measured, a terminal run over the residual
|
|
1641
|
+
# (sidecar-lost-with-key-present) case printed three routine `note:`
|
|
1642
|
+
# lines and exited 0 with no hint `loadFiles` had gone stale. Always
|
|
1643
|
+
# shown, even under `--quiet`, for the same reason `comments-dropped` is.
|
|
1644
|
+
for issue in issues:
|
|
1645
|
+
if issue.code == "debug-config.load-files-preserved":
|
|
1646
|
+
lines.append(f"note: {issue.message}")
|
|
1647
|
+
if not quiet:
|
|
1648
|
+
lines.extend(f"note: {n}" for n in notes)
|
|
1649
|
+
return lines
|
|
1650
|
+
|
|
1651
|
+
|
|
1652
|
+
def _run(
|
|
1653
|
+
*,
|
|
1654
|
+
target_kind: str | None,
|
|
1655
|
+
server_arg: str | None,
|
|
1656
|
+
core: str | None,
|
|
1657
|
+
pre_launch_task: str | None,
|
|
1658
|
+
gdbserver_address: str | None,
|
|
1659
|
+
svd: str | None,
|
|
1660
|
+
preview: bool,
|
|
1661
|
+
project_arg: str,
|
|
1662
|
+
board_yaml_arg: str | None,
|
|
1663
|
+
sdk_root_arg: str | None,
|
|
1664
|
+
quiet: bool,
|
|
1665
|
+
) -> _Outcome:
|
|
1666
|
+
"""The whole command, as a pure-ish computation returning one outcome.
|
|
1667
|
+
|
|
1668
|
+
Nothing here emits or exits; [`debug_config`] does both exactly once. That
|
|
1669
|
+
split is what lets the exception guard wrap this call without swallowing
|
|
1670
|
+
`typer.Exit`.
|
|
1671
|
+
"""
|
|
1672
|
+
generated_at = _generated_at()
|
|
1673
|
+
# Errors before target/server are even known report a cwd-based launch.json
|
|
1674
|
+
# path and a zephyr-mcu/none placeholder (matches the TS catch block).
|
|
1675
|
+
cwd_launch_path = os.path.join(os.getcwd(), ".vscode", "launch.json")
|
|
1676
|
+
|
|
1677
|
+
workspace_root = _normalise(project_arg)
|
|
1678
|
+
launch_json_path = os.path.join(workspace_root, ".vscode", "launch.json")
|
|
1679
|
+
# tan-cli#476, FIRST: a `--project` that does not exist is refused, not
|
|
1680
|
+
# created. Everything below this point can write, and the writer uses
|
|
1681
|
+
# `mkdir(parents=True)`.
|
|
1682
|
+
#
|
|
1683
|
+
# Review round: `os.path.isdir` alone cannot tell "missing" apart from
|
|
1684
|
+
# "exists but is a file", so the two get their own message rather than
|
|
1685
|
+
# both being told they "do not exist" -- a `--project` pointing at, say,
|
|
1686
|
+
# `board.yaml` genuinely exists, just not as a directory. And the
|
|
1687
|
+
# parenthetical resolved-path suffix is dropped when `project_arg` was
|
|
1688
|
+
# already absolute, since `_normalise` would otherwise repeat it verbatim.
|
|
1689
|
+
if not os.path.isdir(workspace_root):
|
|
1690
|
+
resolved_suffix = (
|
|
1691
|
+
"" if os.path.isabs(project_arg) else f" ({workspace_root})"
|
|
1692
|
+
)
|
|
1693
|
+
if os.path.exists(workspace_root):
|
|
1694
|
+
reason = "is not a directory"
|
|
1695
|
+
else:
|
|
1696
|
+
reason = "does not exist"
|
|
1697
|
+
return _project_not_found_failure(
|
|
1698
|
+
generated_at,
|
|
1699
|
+
f"--project {project_arg!r} {reason}{resolved_suffix}. "
|
|
1700
|
+
f"Name a project directory that already exists; debug-config "
|
|
1701
|
+
f"never creates one.",
|
|
1702
|
+
launch_json_path,
|
|
1703
|
+
)
|
|
1704
|
+
(
|
|
1705
|
+
project_root,
|
|
1706
|
+
board_yaml,
|
|
1707
|
+
sdk_root,
|
|
1708
|
+
sdk_source_tier,
|
|
1709
|
+
foreign_global_default_for,
|
|
1710
|
+
) = _resolve_project_reporting_fields(project_arg, board_yaml_arg, sdk_root_arg)
|
|
1711
|
+
# tan-cli#236, the pair of #170 above: `boardYaml` reported only when a
|
|
1712
|
+
# file is really at the resolved path.
|
|
1713
|
+
project = Project.resolved(project_root, board_yaml)
|
|
1714
|
+
|
|
1715
|
+
# tan-cli#456: an omitted --target-kind must never silently default to
|
|
1716
|
+
# native-host on a project whose own build cannot produce that binary --
|
|
1717
|
+
# infer it from the project instead, and pick a matching --server default
|
|
1718
|
+
# too (leaving it at the literal "none" default would only trade one
|
|
1719
|
+
# broken draft for `create_launch_draft`'s "Unsupported debug backend
|
|
1720
|
+
# 'none'" refusal). Never overrides a target/server the caller named
|
|
1721
|
+
# explicitly. `infer_target_kind` itself is pure and shared (`tan.core.
|
|
1722
|
+
# debug_launch`); this is just its IO, the same best-effort reads as
|
|
1723
|
+
# everything else in this module.
|
|
1724
|
+
effective_target_kind = target_kind
|
|
1725
|
+
effective_server_arg = server_arg
|
|
1726
|
+
inferred: str | None = None
|
|
1727
|
+
if target_kind is None:
|
|
1728
|
+
manifest = _load_yaml(Path(workspace_root, "build", "system-manifest.yaml"))
|
|
1729
|
+
inferred, reason_code, ambiguous = infer_target_kind(
|
|
1730
|
+
manifest, core, _board_som_sku(board_yaml)
|
|
1731
|
+
)
|
|
1732
|
+
if ambiguous is not None:
|
|
1733
|
+
# `launch_json_path` -- this project's own -- not `cwd_launch_path`:
|
|
1734
|
+
# unlike the pre-#456 catch-all below, it is already resolved by
|
|
1735
|
+
# this point, so a cwd-based path here would just name whatever
|
|
1736
|
+
# directory the shell happened to be in.
|
|
1737
|
+
#
|
|
1738
|
+
# tan-cli#462: `reason_code`, when set, names which of
|
|
1739
|
+
# `infer_target_kind`'s refusals the caller can fix themselves --
|
|
1740
|
+
# VALIDATION_FAILURE (2), not INTERNAL_FAILURE (5). Review round:
|
|
1741
|
+
# its other two ambiguity shapes (more than one target class, or
|
|
1742
|
+
# none at all) are the SAME defect, not "unclassified" -- both
|
|
1743
|
+
# now carry their own code too, so this dispatches all four.
|
|
1744
|
+
if reason_code == "build-manifest-missing":
|
|
1745
|
+
return _build_manifest_missing_failure(generated_at, ambiguous, launch_json_path)
|
|
1746
|
+
if reason_code == "core-unknown":
|
|
1747
|
+
return _core_unknown_failure(generated_at, ambiguous, launch_json_path)
|
|
1748
|
+
if reason_code == "target-kind-ambiguous":
|
|
1749
|
+
return _target_kind_ambiguous_failure(generated_at, ambiguous, launch_json_path)
|
|
1750
|
+
if reason_code == "no-debuggable-target-class":
|
|
1751
|
+
return _no_debuggable_target_class_failure(
|
|
1752
|
+
generated_at, ambiguous, launch_json_path
|
|
1753
|
+
)
|
|
1754
|
+
return _internal_failure(generated_at, ambiguous, launch_json_path)
|
|
1755
|
+
if inferred is None:
|
|
1756
|
+
# tan-cli#476 half (b): NO signal at all -- `infer_target_kind`
|
|
1757
|
+
# returned `(None, None, None)`, its "nothing to go on" answer.
|
|
1758
|
+
# Falling through here reached `parse_target_kind(None)`, whose
|
|
1759
|
+
# `native-host` default then wrote an `Alp: Native Sim Debug`
|
|
1760
|
+
# entry into whatever directory `--project` happened to name.
|
|
1761
|
+
# That default is only defensible where the project SAYS it is a
|
|
1762
|
+
# native_sim project, which `infer_target_kind` answers with a
|
|
1763
|
+
# real `NATIVE_HOST` of its own, not with "no signal".
|
|
1764
|
+
return _target_kind_unresolved_failure(
|
|
1765
|
+
generated_at,
|
|
1766
|
+
f"--target-kind was not given, and {workspace_root} offers "
|
|
1767
|
+
"nothing to infer one from: it has no "
|
|
1768
|
+
"build/system-manifest.yaml, and no board.yaml declaring "
|
|
1769
|
+
"som.sku. Pass --target-kind explicitly (one of: "
|
|
1770
|
+
f"{', '.join(TARGET_KINDS)}) -- debug-config no longer "
|
|
1771
|
+
"defaults to native-host, which wrote a native_sim launch "
|
|
1772
|
+
"configuration into any directory it was pointed at "
|
|
1773
|
+
"(tan-cli#476). If this really is the project you meant, "
|
|
1774
|
+
"run tan build first, or check you are in the right "
|
|
1775
|
+
"directory.",
|
|
1776
|
+
launch_json_path,
|
|
1777
|
+
)
|
|
1778
|
+
effective_target_kind = inferred
|
|
1779
|
+
if server_arg is None:
|
|
1780
|
+
effective_server_arg = _DEFAULT_SERVER_FOR_TARGET.get(inferred)
|
|
1781
|
+
|
|
1782
|
+
try:
|
|
1783
|
+
target = parse_target_kind(effective_target_kind)
|
|
1784
|
+
server = parse_server_kind(effective_server_arg)
|
|
1785
|
+
except DebugConfigError as err:
|
|
1786
|
+
# Neither local is bound yet -- the zephyr-mcu/none placeholder is the
|
|
1787
|
+
# honest answer here (matches the TS catch block).
|
|
1788
|
+
return _invalid_argument_failure(generated_at, str(err), cwd_launch_path)
|
|
1789
|
+
|
|
1790
|
+
try:
|
|
1791
|
+
draft = create_launch_draft(target, server, pre_launch_task)
|
|
1792
|
+
except DebugConfigError as err:
|
|
1793
|
+
# #508 review, Major 4 follow-up: `target`/`server` ARE already bound
|
|
1794
|
+
# by this point (both parsed above) -- an unsupported PAIRING of two
|
|
1795
|
+
# individually-valid values must report the pairing it actually
|
|
1796
|
+
# refused, not the placeholder. Same reasoning as the `--svd` /
|
|
1797
|
+
# `--gdbserver-address` sites below, which already do this.
|
|
1798
|
+
return _invalid_argument_failure(
|
|
1799
|
+
generated_at, str(err), cwd_launch_path, target, server
|
|
1800
|
+
)
|
|
1801
|
+
|
|
1802
|
+
# tan-cli#964 review (major 5): collects every `som-preset-v2`/
|
|
1803
|
+
# `soc-spec-v1` schema violation found while EITHER SDK-metadata walk
|
|
1804
|
+
# below reads this project's SoM preset/SoC JSON -- `_sdk_published_cores`
|
|
1805
|
+
# (the --core guard, just below) and `_fill_debug_probe_identity_from_sdk`
|
|
1806
|
+
# (the identity fallback, further down) share this one list rather than
|
|
1807
|
+
# each collecting -- and reporting -- its own, so a violation both walks
|
|
1808
|
+
# would hit is not folded into two `debug-config.metadata-schema-invalid`
|
|
1809
|
+
# issues. Before this, `tan debug-config` passed no `warnings` to either
|
|
1810
|
+
# walk and validated NOTHING, despite #964's PR body claiming it inherited
|
|
1811
|
+
# the WARN half "transitively" through `read_sdk_som_and_soc`.
|
|
1812
|
+
schema_warnings: list[str] = []
|
|
1813
|
+
# tan-cli#964 review (major 6, "skip-but-disclose"): the same shared-list
|
|
1814
|
+
# convention as `schema_warnings` above, for the OTHER half -- a note
|
|
1815
|
+
# when the schema file itself is simply absent, rather than the silent
|
|
1816
|
+
# `[]` that used to be indistinguishable from "validated clean".
|
|
1817
|
+
schema_skipped: list[str] = []
|
|
1818
|
+
|
|
1819
|
+
# tan-cli#489 (5): an EXPLICIT --target-kind bypasses `infer_target_kind`
|
|
1820
|
+
# entirely, so its own --core-vs-manifest guard (the `core-unknown` refusal
|
|
1821
|
+
# above) never runs for this path. Without this check, --core naming no
|
|
1822
|
+
# slice in this project's own build sailed through in silence: exit 0, a
|
|
1823
|
+
# placeholder `device`, and an `executable` pointing at a path this
|
|
1824
|
+
# project's build never produced. Checked against the WHOLE manifest (any
|
|
1825
|
+
# os), mirroring `infer_target_kind`'s own guard exactly.
|
|
1826
|
+
#
|
|
1827
|
+
# tan-cli#477 major 2 closes the half #489 left open: with NO manifest at
|
|
1828
|
+
# all this used to stay silent, so an unknown --core on a real hardware
|
|
1829
|
+
# project sailed through at exit 0 with `device` left as the literal
|
|
1830
|
+
# `<resolved-device>` -- a launch.json that looks valid and fails later in
|
|
1831
|
+
# the debugger with nothing connecting the failure back to this command.
|
|
1832
|
+
# The reason it was left open was that `--core` pre-build has a SECOND,
|
|
1833
|
+
# legitimate job -- selecting which core's SDK-published debug-probe
|
|
1834
|
+
# identity to resolve (alp-sdk#1026, `identity_core = core or
|
|
1835
|
+
# build_core_id` below) -- which a guard keyed only on "does a manifest
|
|
1836
|
+
# exist" cannot tell apart from a typo "without also consulting the SDK's
|
|
1837
|
+
# own published core list". So it consults it: `_sdk_published_cores`
|
|
1838
|
+
# reads the same `--sdk-root` the identity fallback already does.
|
|
1839
|
+
#
|
|
1840
|
+
# The two authorities are ordered, never merged: a real build is the truth
|
|
1841
|
+
# about which cores THIS project produced, so when a manifest exists it
|
|
1842
|
+
# alone decides (a SoM core the project does not build must still be
|
|
1843
|
+
# refused). The SDK list stands in only where there is no manifest.
|
|
1844
|
+
# BOTH arms refuse only what they can PROVE unknown -- no slices / no
|
|
1845
|
+
# resolvable SDK core list means "cannot be asked", and staying silent
|
|
1846
|
+
# there is the existing, correct "not built yet" behaviour every other
|
|
1847
|
+
# placeholder-carrying draft already has. `--core` is not a per-server
|
|
1848
|
+
# flag, so this is server-independent: measured pre-fix, the openocd arm
|
|
1849
|
+
# reported only `sdk-identity-key-absent` (silent about the core) and the
|
|
1850
|
+
# pyocd arm reported `issues: []`.
|
|
1851
|
+
if target_kind is not None and core is not None:
|
|
1852
|
+
build_manifest = _load_yaml(Path(workspace_root, "build", "system-manifest.yaml"))
|
|
1853
|
+
all_slices = manifest_slices(build_manifest)
|
|
1854
|
+
if all_slices:
|
|
1855
|
+
if not any(s.get("core_id") == core for s in all_slices):
|
|
1856
|
+
return _explicit_core_unknown_failure(
|
|
1857
|
+
generated_at,
|
|
1858
|
+
target,
|
|
1859
|
+
server,
|
|
1860
|
+
explicit_core_unknown_message(core, all_slices),
|
|
1861
|
+
launch_json_path,
|
|
1862
|
+
)
|
|
1863
|
+
else:
|
|
1864
|
+
# NOT `sdk_root` directly: only a checkout this project is entitled
|
|
1865
|
+
# to be judged by may turn a `--core` into an exit-2 refusal. See
|
|
1866
|
+
# `_sdk_core_refusal_authority` (tan-cli#477 REVIEW round) -- the
|
|
1867
|
+
# identity fallback further down still reads `sdk_root` whatever
|
|
1868
|
+
# its provenance, because filling a placeholder from the wrong
|
|
1869
|
+
# checkout leaves a placeholder, while refusing on its word stops
|
|
1870
|
+
# the run.
|
|
1871
|
+
refusal_sdk = _sdk_core_refusal_authority(
|
|
1872
|
+
sdk_root, sdk_source_tier, foreign_global_default_for
|
|
1873
|
+
)
|
|
1874
|
+
published_cores = _sdk_published_cores(
|
|
1875
|
+
refusal_sdk, board_yaml, warnings=schema_warnings, skipped=schema_skipped
|
|
1876
|
+
)
|
|
1877
|
+
if published_cores and core not in published_cores:
|
|
1878
|
+
return _explicit_core_unknown_failure(
|
|
1879
|
+
generated_at,
|
|
1880
|
+
target,
|
|
1881
|
+
server,
|
|
1882
|
+
_sdk_core_unknown_message(
|
|
1883
|
+
core, published_cores, str(refusal_sdk), sdk_source_tier
|
|
1884
|
+
),
|
|
1885
|
+
launch_json_path,
|
|
1886
|
+
)
|
|
1887
|
+
|
|
1888
|
+
# tan-cli#1488: several cores of this class and no --core -> refuse instead
|
|
1889
|
+
# of silently programming the first slice's core.
|
|
1890
|
+
if core is None:
|
|
1891
|
+
multi_core = multi_core_without_core_message(
|
|
1892
|
+
target, manifest_slices(_load_yaml(Path(workspace_root, "build", "system-manifest.yaml")))
|
|
1893
|
+
)
|
|
1894
|
+
if multi_core is not None:
|
|
1895
|
+
return _core_required_failure(
|
|
1896
|
+
generated_at, target, server, multi_core, launch_json_path
|
|
1897
|
+
)
|
|
1898
|
+
|
|
1899
|
+
# Fill the `<resolved-...>` placeholders from what this project's own build
|
|
1900
|
+
# recorded (#66). Nothing here fails the command: pre-build, or against a
|
|
1901
|
+
# Zephyr that reshaped `runners.yaml`, the draft keeps its placeholders.
|
|
1902
|
+
resolution, registered_runners, build_core_id = _resolve_from_build(
|
|
1903
|
+
workspace_root, target, server, core
|
|
1904
|
+
)
|
|
1905
|
+
|
|
1906
|
+
# alp-sdk#1026: whatever the build did NOT already resolve, try the SDK's
|
|
1907
|
+
# published per-variant debug-probe identity next -- `--core` if given,
|
|
1908
|
+
# else the core id the build itself just resolved. See
|
|
1909
|
+
# `_fill_debug_probe_identity_from_sdk`'s own docstring for why `device`
|
|
1910
|
+
# stays the placeholder on a never-built project with no `--core`.
|
|
1911
|
+
identity_core = core or build_core_id
|
|
1912
|
+
# Snapshot of what the BUILD (not the SDK) resolved, taken before the SDK
|
|
1913
|
+
# fallback runs, so `sdk_filled_json_fields` below can name exactly the
|
|
1914
|
+
# fields the SDK fallback itself just populated -- never a field a real
|
|
1915
|
+
# build's `runners.yaml` already resolved (that overwrite is pre-existing,
|
|
1916
|
+
# intended behaviour per `_merge_configuration`'s own doc comment, not
|
|
1917
|
+
# something this disclosure is scoped to cover).
|
|
1918
|
+
device_before_identity = resolution.device
|
|
1919
|
+
target_id_before_identity = resolution.target_id
|
|
1920
|
+
config_files_empty_before_identity = not resolution.config_files
|
|
1921
|
+
identity_debug_block_found, known_jlink_cores = _fill_debug_probe_identity_from_sdk(
|
|
1922
|
+
resolution, sdk_root, board_yaml, identity_core,
|
|
1923
|
+
warnings=schema_warnings, skipped=schema_skipped,
|
|
1924
|
+
)
|
|
1925
|
+
# Which launch-configuration JSON keys the SDK fallback (not a real build)
|
|
1926
|
+
# just populated -- the ONLY fields `sdk_identity_overwrites` below is
|
|
1927
|
+
# allowed to flag (alp-sdk#1026 review finding #1).
|
|
1928
|
+
sdk_filled_json_fields: list[str] = []
|
|
1929
|
+
if device_before_identity is None and resolution.device is not None:
|
|
1930
|
+
sdk_filled_json_fields.append("device")
|
|
1931
|
+
if target_id_before_identity is None and resolution.target_id is not None:
|
|
1932
|
+
sdk_filled_json_fields.append("targetId")
|
|
1933
|
+
if config_files_empty_before_identity and resolution.config_files:
|
|
1934
|
+
sdk_filled_json_fields.append("configFiles")
|
|
1935
|
+
|
|
1936
|
+
# `--svd` is the ONLY producer of `resolution.svd` (tan-cli#197): the SDK
|
|
1937
|
+
# ships no SVD, so without the flag the field is structurally always absent
|
|
1938
|
+
# and `apply_launch_resolution` drops both svd keys.
|
|
1939
|
+
if svd is not None:
|
|
1940
|
+
try:
|
|
1941
|
+
resolution.svd = _resolve_user_svd(workspace_root, svd)
|
|
1942
|
+
except DebugConfigError as err:
|
|
1943
|
+
# tan-cli#477 review: `target`/`server` ARE known here, unlike at
|
|
1944
|
+
# the parse site above -- reporting the zephyr-mcu/none
|
|
1945
|
+
# placeholder would misdescribe what the caller asked for, and a
|
|
1946
|
+
# validation verdict the extension may render must not.
|
|
1947
|
+
return _invalid_argument_failure(
|
|
1948
|
+
generated_at, str(err), launch_json_path, target, server
|
|
1949
|
+
)
|
|
1950
|
+
|
|
1951
|
+
# `--gdbserver-address` is the ONLY producer of `resolution.gdbserver_address`
|
|
1952
|
+
# (tan-cli#321): a runtime property of the deployed board, so nothing else
|
|
1953
|
+
# -- not a build, not SDK-published metadata -- can ever fill it.
|
|
1954
|
+
if gdbserver_address is not None:
|
|
1955
|
+
try:
|
|
1956
|
+
resolution.gdbserver_address = _resolve_gdbserver_address(gdbserver_address)
|
|
1957
|
+
except DebugConfigError as err:
|
|
1958
|
+
# Same as `--svd` above: both are known by this point.
|
|
1959
|
+
return _invalid_argument_failure(
|
|
1960
|
+
generated_at, str(err), launch_json_path, target, server
|
|
1961
|
+
)
|
|
1962
|
+
|
|
1963
|
+
apply_launch_resolution(draft, resolution)
|
|
1964
|
+
|
|
1965
|
+
# alp-sdk#1026 review finding #4: which server-identity field the SDK
|
|
1966
|
+
# fallback found a block for but could not fill -- checked against the
|
|
1967
|
+
# FINAL draft (after `apply_launch_resolution`), not the pre-resolution
|
|
1968
|
+
# one, or a field the fallback itself just resolved would misreport as
|
|
1969
|
+
# still absent. Advisory about resolution state, so it fires on
|
|
1970
|
+
# `--preview` too, not just a write.
|
|
1971
|
+
identity_issues: list[Issue] = [
|
|
1972
|
+
# tan-cli#964 review (major 5): the WARN half of #964's decided rule,
|
|
1973
|
+
# same shape `presets_cmd.py`/`size_cmd.py` already use -- one issue
|
|
1974
|
+
# per violation, naming the file, the JSON pointer, and what was
|
|
1975
|
+
# found. `debug-config` already degrades a schema-invalid preset/SoC
|
|
1976
|
+
# JSON to its existing placeholder behaviour (nothing above refuses
|
|
1977
|
+
# on `schema_warnings`); this makes that degrade visible instead of
|
|
1978
|
+
# silent.
|
|
1979
|
+
Issue("debug-config.metadata-schema-invalid", "warning", w)
|
|
1980
|
+
for w in schema_warnings
|
|
1981
|
+
]
|
|
1982
|
+
# tan-cli#964 review (major 6, "skip-but-disclose"): `info`, not
|
|
1983
|
+
# `warning` -- nothing here says a document is wrong, only that a schema
|
|
1984
|
+
# to check it against is absent. Deduplicated: both walks above can each
|
|
1985
|
+
# find the same missing schema.
|
|
1986
|
+
identity_issues.extend(
|
|
1987
|
+
Issue("debug-config.metadata-schema-unchecked", "info", w)
|
|
1988
|
+
for w in dict.fromkeys(schema_skipped)
|
|
1989
|
+
)
|
|
1990
|
+
if identity_debug_block_found:
|
|
1991
|
+
field = _SERVER_IDENTITY_FIELD.get(server)
|
|
1992
|
+
if field is not None and _has_placeholder(draft.get(field)):
|
|
1993
|
+
# tan-cli#489 (4): `jlink_device` is the ONE field this identity
|
|
1994
|
+
# keys by core (`configFiles`/`targetId` are not core-dependent at
|
|
1995
|
+
# all -- `sdk-identity-key-absent` is always the right call for
|
|
1996
|
+
# those). For `device`, a placeholder surviving despite a found
|
|
1997
|
+
# debug block means the lookup itself never had a usable key: no
|
|
1998
|
+
# core was resolved, or the one resolved has no entry in the
|
|
1999
|
+
# published map -- never "the SDK publishes no value for this
|
|
2000
|
+
# SoM", which is what `sdk-identity-key-absent` would have said.
|
|
2001
|
+
#
|
|
2002
|
+
# Review round: `bool(known_jlink_cores)` is required in front of
|
|
2003
|
+
# the core-mismatch half. A SoM whose published `variants[].debug`
|
|
2004
|
+
# carries NO `jlink_device` key at all (real shape: today's Alif
|
|
2005
|
+
# entries publish `openocd_config` but not `jlink_device`) makes
|
|
2006
|
+
# `known_jlink_cores` the empty set regardless of `identity_core`
|
|
2007
|
+
# -- `core_unindexed` was unconditionally True, so even a VALID
|
|
2008
|
+
# `--core m55_hp` reported "has no `device` entry for core
|
|
2009
|
+
# 'm55_hp' -- its published cores are: none ... pass --core with
|
|
2010
|
+
# one of the cores above", advice that contradicts itself. That
|
|
2011
|
+
# SoM genuinely publishes no `device` value for ANY core --
|
|
2012
|
+
# `sdk-identity-key-absent` is the correct, and only correct,
|
|
2013
|
+
# code for it, exactly as it was before this split.
|
|
2014
|
+
core_unindexed = bool(known_jlink_cores) and (
|
|
2015
|
+
identity_core is None or identity_core not in known_jlink_cores
|
|
2016
|
+
)
|
|
2017
|
+
if server == JLINK and core_unindexed:
|
|
2018
|
+
identity_issues.append(
|
|
2019
|
+
_sdk_identity_core_unresolved_issue(identity_core, known_jlink_cores)
|
|
2020
|
+
)
|
|
2021
|
+
else:
|
|
2022
|
+
identity_issues.append(_sdk_identity_key_absent_issue(field))
|
|
2023
|
+
notes = _preview_notes_for(draft, registered_runners, server, target)
|
|
2024
|
+
# tan-cli#456 review: say when target/server were DERIVED, not requested --
|
|
2025
|
+
# otherwise silent, unlike the --svd/--gdbserver-address no-op notes right
|
|
2026
|
+
# below. Never fires for the no-signal native-host default (no manifest to
|
|
2027
|
+
# name).
|
|
2028
|
+
if target_kind is None and inferred is not None:
|
|
2029
|
+
server_clause = f" and --server '{effective_server_arg}'" if server_arg is None else ""
|
|
2030
|
+
notes.append(
|
|
2031
|
+
f"--target-kind was not given; inferred '{inferred}'{server_clause} from "
|
|
2032
|
+
"this project's build/system-manifest.yaml."
|
|
2033
|
+
)
|
|
2034
|
+
# A non-MCU draft carries no `svdFile` key at all, and
|
|
2035
|
+
# `apply_launch_resolution` only replaces keys that already exist -- so a
|
|
2036
|
+
# `--svd` here is a no-op. Say so rather than accepting the flag in silence
|
|
2037
|
+
# and leaving the user to wonder why no peripheral view appeared.
|
|
2038
|
+
if svd is not None and "svdFile" not in draft:
|
|
2039
|
+
notes.append(
|
|
2040
|
+
# `target`, the RESOLVED target -- `target_kind` is `None` on
|
|
2041
|
+
# every tan-cli#456 inference path and would mis-name it.
|
|
2042
|
+
f"--svd was given, but target kind '{target}' emits "
|
|
2043
|
+
"no svdFile field, so it had no effect: the Cortex Peripherals view "
|
|
2044
|
+
"is a cortex-debug (MCU) feature."
|
|
2045
|
+
)
|
|
2046
|
+
# Same "no silent no-op" floor as `--svd` above: only a yocto-userspace
|
|
2047
|
+
# draft carries `miDebuggerServerAddress` at all.
|
|
2048
|
+
if gdbserver_address is not None and "miDebuggerServerAddress" not in draft:
|
|
2049
|
+
notes.append(
|
|
2050
|
+
f"--gdbserver-address was given, but target kind "
|
|
2051
|
+
f"'{target}' emits no miDebuggerServerAddress "
|
|
2052
|
+
"field, so it had no effect: that field is a yocto-userspace "
|
|
2053
|
+
"(cppdbg) feature."
|
|
2054
|
+
)
|
|
2055
|
+
|
|
2056
|
+
def success(
|
|
2057
|
+
*, replaced: bool, configuration: Any, issues: list[Issue], is_preview: bool
|
|
2058
|
+
) -> _Outcome:
|
|
2059
|
+
# tan-cli#321: checked against `configuration` -- the value ACTUALLY
|
|
2060
|
+
# going out (the fresh `draft` on `--preview`, the merged
|
|
2061
|
+
# `written_configuration` on a write) -- not the pre-merge `draft`
|
|
2062
|
+
# this closure captures from its enclosing scope. A write that merged
|
|
2063
|
+
# over a customer's own already-hand-filled address must not re-nag
|
|
2064
|
+
# them every run; checking the final value is what tells the two
|
|
2065
|
+
# apart, the same distinction `_has_placeholder` exists for.
|
|
2066
|
+
final_issues = list(issues)
|
|
2067
|
+
if target == YOCTO_USERSPACE and isinstance(configuration, dict):
|
|
2068
|
+
if _has_placeholder(configuration.get("miDebuggerServerAddress")):
|
|
2069
|
+
final_issues.append(_gdbserver_address_unresolved_issue())
|
|
2070
|
+
return _Outcome(
|
|
2071
|
+
exit_code=ExitCode.SUCCESS,
|
|
2072
|
+
data=_data(
|
|
2073
|
+
generated_at=generated_at,
|
|
2074
|
+
target=target,
|
|
2075
|
+
server=server,
|
|
2076
|
+
preview=is_preview,
|
|
2077
|
+
launch_json_path=launch_json_path,
|
|
2078
|
+
replaced=replaced,
|
|
2079
|
+
notes=notes,
|
|
2080
|
+
configuration=configuration,
|
|
2081
|
+
),
|
|
2082
|
+
project=project,
|
|
2083
|
+
issues=final_issues,
|
|
2084
|
+
text=_success_text(
|
|
2085
|
+
target=target,
|
|
2086
|
+
server=server,
|
|
2087
|
+
launch_json_path=launch_json_path,
|
|
2088
|
+
replaced=replaced,
|
|
2089
|
+
preview=is_preview,
|
|
2090
|
+
notes=notes,
|
|
2091
|
+
configuration=configuration,
|
|
2092
|
+
quiet=quiet,
|
|
2093
|
+
issues=final_issues,
|
|
2094
|
+
),
|
|
2095
|
+
)
|
|
2096
|
+
|
|
2097
|
+
if preview:
|
|
2098
|
+
# `--preview` never merges anything (it returns before the customer's
|
|
2099
|
+
# file is even read), so it reports the fresh draft -- which is also all
|
|
2100
|
+
# there is. tan-cli#180's preview-side invariant, and what the four
|
|
2101
|
+
# `debug-config-preview-*` goldens pin.
|
|
2102
|
+
return success(
|
|
2103
|
+
replaced=False, configuration=draft, issues=identity_issues, is_preview=True
|
|
2104
|
+
)
|
|
2105
|
+
|
|
2106
|
+
# Write mode: merge into .vscode/launch.json.
|
|
2107
|
+
try:
|
|
2108
|
+
Path(launch_json_path).parent.mkdir(parents=True, exist_ok=True)
|
|
2109
|
+
except OSError as err:
|
|
2110
|
+
return _write_failure(
|
|
2111
|
+
generated_at, target, server, launch_json_path, str(err)
|
|
2112
|
+
)
|
|
2113
|
+
|
|
2114
|
+
# A READ error on an EXISTING launch.json (wrong encoding, e.g. UTF-16LE
|
|
2115
|
+
# from PowerShell `>` redirection; a denied ACL; a sharing violation) must
|
|
2116
|
+
# NOT collapse into the same "no file yet" as absence. That fed a fresh
|
|
2117
|
+
# document to the write plan and the write then overwrote the user's file
|
|
2118
|
+
# wholesale -- silently destroying every hand-written debug configuration at
|
|
2119
|
+
# exit 0. The malformed-JSON case below is deliberately guarded (no write);
|
|
2120
|
+
# a read error must refuse to write for the same reason.
|
|
2121
|
+
existing: str | None = None
|
|
2122
|
+
if Path(launch_json_path).exists():
|
|
2123
|
+
try:
|
|
2124
|
+
# `newline=""` on the READ as well as the write: the default
|
|
2125
|
+
# universal-newlines mode translates every `\r\n` to `\n` before the
|
|
2126
|
+
# splice ever sees the text, so `jsonc_splice`'s dominant-EOL check
|
|
2127
|
+
# found no CRLF and re-wrote a Windows-authored launch.json LF-only.
|
|
2128
|
+
# Rust's `read_to_string` does not translate; neither may this.
|
|
2129
|
+
with open(launch_json_path, encoding="utf-8", newline="") as handle:
|
|
2130
|
+
existing = handle.read()
|
|
2131
|
+
except (OSError, UnicodeDecodeError) as err:
|
|
2132
|
+
return _internal_failure(
|
|
2133
|
+
generated_at,
|
|
2134
|
+
f"Alp: failed to read existing .vscode/launch.json: {err}",
|
|
2135
|
+
cwd_launch_path,
|
|
2136
|
+
)
|
|
2137
|
+
|
|
2138
|
+
# tan-cli#518: the `.alp/` provenance sidecar, read the SAME best-effort
|
|
2139
|
+
# way `_write_project_sdk_pointer`'s own reads are (`bootstrap_cmd.py`) --
|
|
2140
|
+
# ANY failure (absent file, a read error, unparsable/unrecognised JSON;
|
|
2141
|
+
# see `launch_provenance.load`'s own docstring) degrades to `empty()`,
|
|
2142
|
+
# never to "everything is ours". This is deliberately NOT held to the
|
|
2143
|
+
# same read-error-must-refuse-to-write bar as `launch.json` itself just
|
|
2144
|
+
# above: losing provenance only makes the NEXT merge more conservative
|
|
2145
|
+
# (append instead of overwrite), never destructive, so there is nothing
|
|
2146
|
+
# here worth refusing the write over.
|
|
2147
|
+
provenance_path = launch_provenance.sidecar_path(workspace_root)
|
|
2148
|
+
provenance_content: str | None = None
|
|
2149
|
+
# tan-cli#1116 review round 2: no `is_file()` pre-flight -- `Path.is_file()`
|
|
2150
|
+
# (pre-3.14) swallows ENOENT/ENOTDIR/EBADF/ELOOP but not EACCES, and on
|
|
2151
|
+
# 3.14+ it swallows EVERY OSError including EACCES and returns `False`,
|
|
2152
|
+
# which would have taken this "absent" arm for a permission-denied
|
|
2153
|
+
# sidecar instead of the "read error" one two paragraphs up documents.
|
|
2154
|
+
# `UnicodeDecodeError` is caught alongside `OSError` for the same reason
|
|
2155
|
+
# named there: it is a `ValueError`, not an `OSError`, so a non-UTF-8
|
|
2156
|
+
# sidecar used to escape this "ANY failure ... degrades to empty()"
|
|
2157
|
+
# contract raw.
|
|
2158
|
+
try:
|
|
2159
|
+
with open(provenance_path, encoding="utf-8") as handle:
|
|
2160
|
+
provenance_content = handle.read()
|
|
2161
|
+
except (OSError, UnicodeDecodeError):
|
|
2162
|
+
provenance_content = None
|
|
2163
|
+
provenance = launch_provenance.load(provenance_content)
|
|
2164
|
+
|
|
2165
|
+
# alp-sdk#1026 review finding #1: compute this BEFORE the write, against
|
|
2166
|
+
# the file as it stood -- `create_launch_json_write_plan` below already
|
|
2167
|
+
# performs the same overwrite (that part of its behaviour is intentional,
|
|
2168
|
+
# see `_merge_configuration`'s own doc comment), this only detects it so
|
|
2169
|
+
# it can be disclosed. tan-cli#518: `provenance` is passed through so a
|
|
2170
|
+
# LIST field (`configFiles`) is only reported as overwritten when the
|
|
2171
|
+
# real merge below would actually do that -- see `sdk_identity_overwrites`'s
|
|
2172
|
+
# own doc comment on why an honest "would this actually happen" beats an
|
|
2173
|
+
# unconditional "the values differ".
|
|
2174
|
+
overwrites = sdk_identity_overwrites(
|
|
2175
|
+
existing, draft, sdk_filled_json_fields, provenance=provenance
|
|
2176
|
+
)
|
|
2177
|
+
# tan-cli#982 review finding #2: the OTHER outcome that same merge can
|
|
2178
|
+
# produce for a list field -- an existing value provenance could not
|
|
2179
|
+
# prove was tan's own is left in place and the new one is appended
|
|
2180
|
+
# beside it, rather than replaced. `sdk_identity_overwrites` above
|
|
2181
|
+
# correctly stays silent about this shape (nothing concrete was lost);
|
|
2182
|
+
# this discloses the append instead, so it is not silent everywhere.
|
|
2183
|
+
stranded_appends = sdk_identity_stranded_appends(
|
|
2184
|
+
existing, draft, sdk_filled_json_fields, provenance=provenance
|
|
2185
|
+
)
|
|
2186
|
+
# tan-cli#1020 review: the SAME "mirror the real merge, then disclose"
|
|
2187
|
+
# discipline as the two calls above, for the ONE field neither of them
|
|
2188
|
+
# covers -- `loadFiles` is populated by the draft itself (tan-cli#945),
|
|
2189
|
+
# never by the SDK-identity fallback those two are scoped to, and its
|
|
2190
|
+
# own merge rule (`_merge_load_files`) protects a hand-authored value
|
|
2191
|
+
# WHOLESALE rather than appending beside it.
|
|
2192
|
+
preserved_load_files = load_files_preserved(existing, draft, provenance)
|
|
2193
|
+
|
|
2194
|
+
# tan-cli#489 (6): `--pre-launch-task ''` opts OUT of a `preLaunchTask` key
|
|
2195
|
+
# entirely (`create_launch_draft` builds it, then deletes it), which is
|
|
2196
|
+
# indistinguishable, to the merge's own "only visit the draft's own keys"
|
|
2197
|
+
# rule, from this target simply having no default -- so the opt-out was a
|
|
2198
|
+
# silent no-op against an entry a PRIOR run already gave one. Named here,
|
|
2199
|
+
# explicitly, rather than left for `create_launch_json_write_plan` to
|
|
2200
|
+
# infer from the draft alone.
|
|
2201
|
+
explicit_omissions = frozenset({"preLaunchTask"}) if pre_launch_task == "" else frozenset()
|
|
2202
|
+
|
|
2203
|
+
try:
|
|
2204
|
+
plan = create_launch_json_write_plan(
|
|
2205
|
+
existing, draft, explicit_omissions, provenance=provenance
|
|
2206
|
+
)
|
|
2207
|
+
except DebugConfigError as err:
|
|
2208
|
+
# A malformed existing launch.json surfaces as an internal failure in TS.
|
|
2209
|
+
return _internal_failure(generated_at, str(err), cwd_launch_path)
|
|
2210
|
+
|
|
2211
|
+
# `open(launch_json_path, "w")` truncates the customer's file to zero
|
|
2212
|
+
# before a single byte of `plan.content` is written, and that content
|
|
2213
|
+
# exists only in memory -- a failure between the truncate and the flush
|
|
2214
|
+
# (ENOSPC, a quota/RLIMIT_FSIZE hit, an I/O error, or the process dying:
|
|
2215
|
+
# SIGKILL, power loss, the VS Code extension killing the child) destroys
|
|
2216
|
+
# every hand-written configuration with no way for tan to repair it: the
|
|
2217
|
+
# next run reads the wreckage, hits the malformed-JSON guard above, and
|
|
2218
|
+
# refuses at exit 5 forever. `atomic_write_text`'s own docstring
|
|
2219
|
+
# (`tan/core/atomic_write.py`) covers the rest: symlink-safe, fsync'd
|
|
2220
|
+
# before the rename, and mode-preserving -- shared with
|
|
2221
|
+
# `bootstrap_cmd.reconcile_west_manifest_path` (tan-cli#516) rather than
|
|
2222
|
+
# a second hand-synchronised copy living beside this call site.
|
|
2223
|
+
try:
|
|
2224
|
+
atomic_write_text(launch_json_path, plan.content)
|
|
2225
|
+
except OSError as err:
|
|
2226
|
+
return _write_failure(
|
|
2227
|
+
generated_at, target, server, launch_json_path, str(err)
|
|
2228
|
+
)
|
|
2229
|
+
|
|
2230
|
+
# tan-cli#518: persist the updated provenance sidecar AFTER launch.json
|
|
2231
|
+
# itself is safely on disk, and best-effort -- a failure here never fails
|
|
2232
|
+
# the command (the launch.json write is the one that matters and it
|
|
2233
|
+
# already succeeded) and never leaves a half-written sidecar
|
|
2234
|
+
# (`atomic_write_text` again), it just means the NEXT run degrades back
|
|
2235
|
+
# to `launch_provenance.empty()`'s conservative default for whatever this
|
|
2236
|
+
# write recorded, exactly as if this run had never touched the sidecar
|
|
2237
|
+
# at all.
|
|
2238
|
+
try:
|
|
2239
|
+
provenance_path.parent.mkdir(parents=True, exist_ok=True)
|
|
2240
|
+
atomic_write_text(str(provenance_path), launch_provenance.render(plan.provenance))
|
|
2241
|
+
except OSError:
|
|
2242
|
+
pass
|
|
2243
|
+
|
|
2244
|
+
issues: list[Issue] = list(identity_issues)
|
|
2245
|
+
if plan.migrated_from is not None:
|
|
2246
|
+
issues.append(_migrated_issue(plan.migrated_from, draft.get("name", "")))
|
|
2247
|
+
if plan.legacy_entry_present is not None:
|
|
2248
|
+
issues.append(_legacy_untouched_issue(plan.legacy_entry_present))
|
|
2249
|
+
if plan.comments_dropped:
|
|
2250
|
+
issues.append(_comments_dropped_issue())
|
|
2251
|
+
# alp-sdk#1026 review finding #1: this write just replaced a concrete
|
|
2252
|
+
# existing value with one resolved from the SDK's published debug-probe
|
|
2253
|
+
# identity rather than a real build -- say so, the same way a dropped
|
|
2254
|
+
# comment is disclosed rather than left for the customer to notice by
|
|
2255
|
+
# diffing the file themselves.
|
|
2256
|
+
for field, existing_value, incoming_value in overwrites:
|
|
2257
|
+
issues.append(
|
|
2258
|
+
_sdk_identity_overwrite_issue(field, existing_value, incoming_value)
|
|
2259
|
+
)
|
|
2260
|
+
for field, existing_value, incoming_value in stranded_appends:
|
|
2261
|
+
issues.append(
|
|
2262
|
+
_sdk_identity_appended_issue(field, existing_value, incoming_value)
|
|
2263
|
+
)
|
|
2264
|
+
if preserved_load_files is not None:
|
|
2265
|
+
issues.append(_load_files_preserved_issue(*preserved_load_files))
|
|
2266
|
+
|
|
2267
|
+
return success(
|
|
2268
|
+
replaced=plan.replaced,
|
|
2269
|
+
# tan-cli#180: report what this write actually put in the file -- the
|
|
2270
|
+
# merged/migrated result -- never the fresh `draft`, which still carries
|
|
2271
|
+
# its own `<resolved-...>` placeholders even after a merge resolved them
|
|
2272
|
+
# from the customer's real, hand-filled values.
|
|
2273
|
+
configuration=plan.written_configuration,
|
|
2274
|
+
issues=issues,
|
|
2275
|
+
is_preview=False,
|
|
2276
|
+
)
|
|
2277
|
+
|
|
2278
|
+
|
|
2279
|
+
def debug_config(
|
|
2280
|
+
ctx: typer.Context,
|
|
2281
|
+
target_kind: str = typer.Option(
|
|
2282
|
+
None,
|
|
2283
|
+
"--target-kind",
|
|
2284
|
+
metavar="KIND",
|
|
2285
|
+
help="Debug target class (zephyr-mcu, baremetal-mcu, yocto-userspace, native-host).",
|
|
2286
|
+
),
|
|
2287
|
+
server: str = typer.Option(
|
|
2288
|
+
None,
|
|
2289
|
+
"--server",
|
|
2290
|
+
metavar="SERVER",
|
|
2291
|
+
help="Debug server backend (jlink, openocd, pyocd, gdbserver, none).",
|
|
2292
|
+
),
|
|
2293
|
+
core: str = typer.Option(
|
|
2294
|
+
None,
|
|
2295
|
+
"--core",
|
|
2296
|
+
metavar="CORE_ID",
|
|
2297
|
+
help=(
|
|
2298
|
+
"Resolve against the build slice with this core_id. Defaults to the "
|
|
2299
|
+
"first slice matching the target class's OS."
|
|
2300
|
+
),
|
|
2301
|
+
),
|
|
2302
|
+
pre_launch_task: str = typer.Option(
|
|
2303
|
+
None,
|
|
2304
|
+
"--pre-launch-task",
|
|
2305
|
+
metavar="TASK",
|
|
2306
|
+
help=(
|
|
2307
|
+
"Emit preLaunchTask: <TASK> on the generated configuration. "
|
|
2308
|
+
"Defaults to the v0.3.1 task name for this target (tan-cli#138): "
|
|
2309
|
+
"'alp: build active target' (zephyr-mcu), 'alp: build baremetal "
|
|
2310
|
+
"target' (baremetal-mcu), 'alp: build native_sim target' "
|
|
2311
|
+
"(native-host). yocto-userspace carries no default (tan-cli#321: "
|
|
2312
|
+
"the extension's only registered task for it exits 1 by design) "
|
|
2313
|
+
"-- pass this flag explicitly to add a reminder. Pass an empty "
|
|
2314
|
+
"string to omit the key entirely."
|
|
2315
|
+
),
|
|
2316
|
+
),
|
|
2317
|
+
gdbserver_address: str = typer.Option(
|
|
2318
|
+
None,
|
|
2319
|
+
"--gdbserver-address",
|
|
2320
|
+
metavar="HOST:PORT",
|
|
2321
|
+
help=(
|
|
2322
|
+
"Fill miDebuggerServerAddress on a yocto-userspace configuration "
|
|
2323
|
+
"(tan-cli#321). This is a runtime property of the deployed board "
|
|
2324
|
+
"that no build can resolve; without it the field stays the "
|
|
2325
|
+
"<host>:<port> placeholder and F5 fails at connect."
|
|
2326
|
+
),
|
|
2327
|
+
),
|
|
2328
|
+
svd: str = typer.Option(
|
|
2329
|
+
None,
|
|
2330
|
+
"--svd",
|
|
2331
|
+
metavar="PATH",
|
|
2332
|
+
help=(
|
|
2333
|
+
"Point cortex-debug's Cortex Peripherals view at an SVD file you "
|
|
2334
|
+
"supply. The SDK ships none (alp-sdk#948). A relative path resolves "
|
|
2335
|
+
"against the current directory; an unreadable one fails the command."
|
|
2336
|
+
),
|
|
2337
|
+
),
|
|
2338
|
+
preview: bool = typer.Option(
|
|
2339
|
+
False, "--preview", help="Print the launch configuration without writing launch.json."
|
|
2340
|
+
),
|
|
2341
|
+
project: str = typer.Option(
|
|
2342
|
+
None, "--project", metavar="PATH", help="Project root (defaults to '.')."
|
|
2343
|
+
),
|
|
2344
|
+
board_yaml: str = typer.Option(
|
|
2345
|
+
None, "--board-yaml", metavar="PATH", help="Explicit board.yaml path."
|
|
2346
|
+
),
|
|
2347
|
+
sdk_root: str = typer.Option( # read for the alp-sdk#1026 metadata fallback; see below
|
|
2348
|
+
None, "--sdk-root", metavar="PATH", help="alp-sdk checkout root."
|
|
2349
|
+
),
|
|
2350
|
+
output_format: OutputFormat = typer.Option(None, "--format", help=FORMAT_HELP),
|
|
2351
|
+
quiet: bool = typer.Option(False, "--quiet", help="Suppress non-essential output."),
|
|
2352
|
+
) -> None:
|
|
2353
|
+
"""Generate (or preview) a VS Code launch.json debug configuration.
|
|
2354
|
+
|
|
2355
|
+
`--sdk-root` is read, but only as a best-effort, silent enrichment
|
|
2356
|
+
(alp-sdk#1026): a resolved checkout's `metadata/**` fills whatever the
|
|
2357
|
+
build itself did not already resolve (`_fill_debug_probe_identity_from_sdk`),
|
|
2358
|
+
but is never shelled and never reported as an `sdk` envelope dependency,
|
|
2359
|
+
matching the module docstring above. clap makes `--sdk-root` `global =
|
|
2360
|
+
true` in Rust regardless, so `tan --sdk-root X debug-config` must not be a
|
|
2361
|
+
parse error even when `X` names no real checkout -- an unresolvable value
|
|
2362
|
+
changes nothing in the envelope (the fallback silently finds nothing),
|
|
2363
|
+
same as the oracle.
|
|
2364
|
+
"""
|
|
2365
|
+
# `--format` is accepted BEFORE the subcommand too (`tan --format json
|
|
2366
|
+
# debug-config ...`, which is what the committed goldens invoke and what
|
|
2367
|
+
# clap's `global = true` gives the Rust); the root callback records it and
|
|
2368
|
+
# this option overrides it when repeated after the command name.
|
|
2369
|
+
resolved_format = resolve_format(output_format, ctx.obj, choices=OutputFormat)
|
|
2370
|
+
json_mode = resolved_format == "json"
|
|
2371
|
+
|
|
2372
|
+
try:
|
|
2373
|
+
outcome = _run(
|
|
2374
|
+
target_kind=target_kind,
|
|
2375
|
+
server_arg=server,
|
|
2376
|
+
core=core,
|
|
2377
|
+
pre_launch_task=pre_launch_task,
|
|
2378
|
+
gdbserver_address=gdbserver_address,
|
|
2379
|
+
svd=svd,
|
|
2380
|
+
preview=preview,
|
|
2381
|
+
project_arg=project or ".",
|
|
2382
|
+
board_yaml_arg=board_yaml,
|
|
2383
|
+
sdk_root_arg=sdk_root,
|
|
2384
|
+
quiet=quiet,
|
|
2385
|
+
)
|
|
2386
|
+
except Exception as err: # noqa: BLE001
|
|
2387
|
+
# The recurring break this guard exists for: an escaping traceback puts
|
|
2388
|
+
# nothing parseable on stdout, and the extension renders an empty panel
|
|
2389
|
+
# with no error at all. Any unforeseen failure becomes a coded envelope.
|
|
2390
|
+
# `typer.Exit` cannot reach here -- `_run` never raises it (it returns an
|
|
2391
|
+
# outcome) -- which is why the emit/exit pair lives outside this try:
|
|
2392
|
+
# `typer.Exit` subclasses RuntimeError, not SystemExit, so a bare
|
|
2393
|
+
# `except Exception` around it would swallow the process exit.
|
|
2394
|
+
outcome = _internal_failure(
|
|
2395
|
+
_generated_at(),
|
|
2396
|
+
f"debug-config failed unexpectedly: {err}",
|
|
2397
|
+
os.path.join(os.getcwd(), ".vscode", "launch.json"),
|
|
2398
|
+
)
|
|
2399
|
+
|
|
2400
|
+
if json_mode:
|
|
2401
|
+
emit(
|
|
2402
|
+
Envelope(
|
|
2403
|
+
"debug-config",
|
|
2404
|
+
outcome.project,
|
|
2405
|
+
outcome.data,
|
|
2406
|
+
outcome.issues,
|
|
2407
|
+
outcome.exit_code,
|
|
2408
|
+
)
|
|
2409
|
+
)
|
|
2410
|
+
else:
|
|
2411
|
+
# stdout is the envelope channel and carries nothing else, in either
|
|
2412
|
+
# mode; stderr carries no contract of its own.
|
|
2413
|
+
stream = typer.get_text_stream("stderr")
|
|
2414
|
+
for line in outcome.text:
|
|
2415
|
+
stream.write(f"{line}\n")
|
|
2416
|
+
raise typer.Exit(int(outcome.exit_code))
|
|
2417
|
+
|
|
2418
|
+
|
|
2419
|
+
# tan-cli#261: adds the six oracle `GlobalArgs` flags this command was still
|
|
2420
|
+
# missing (`--all`/`--ci`/`--no-color`/`--non-interactive`/`--target`/
|
|
2421
|
+
# `--verbose`) on top of `--quiet`, already declared and read above; see
|
|
2422
|
+
# `tan.core.global_flags`. `ctx: typer.Context` (this command's own
|
|
2423
|
+
# `_HONOURS_ROOT_FORMAT` seam) is untouched -- appended parameters are all
|
|
2424
|
+
# keyword-only Options, never repositioned relative to it.
|
|
2425
|
+
debug_config = accept_global_flags(debug_config)
|