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,1073 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""`tan support-bundle` -- export a diagnostic bundle (inspect + trace +
|
|
3
|
+
doctor) to a JSON file for attaching to a bug report.
|
|
4
|
+
|
|
5
|
+
Port of `crates/tan-cli/src/commands/support_bundle.rs`. Composes three
|
|
6
|
+
sections into one written file: the resolved debug context + its resolved
|
|
7
|
+
values (`inspect_cmd`'s own model), the generation-trace decisions
|
|
8
|
+
(`trace_cmd`'s own model), and a doctor report -- then returns a stdout
|
|
9
|
+
envelope naming the written path + a decision count. Exit follows the
|
|
10
|
+
oracle's rule verbatim: `DOCTOR_FAILURE` (4) when the bundled doctor
|
|
11
|
+
section's `summary.fail > 0` (`support_bundle.rs`: `let exit = if
|
|
12
|
+
doctor.summary.fail > 0 { ExitCode::DoctorFailure }`), else `SUCCESS` (0).
|
|
13
|
+
The bundle file is still written either way. An unsupported target/server
|
|
14
|
+
pairing -> `DOCTOR_FAILURE` (4) too; a bad `--target-kind`/`--server` value
|
|
15
|
+
-> `INTERNAL_FAILURE` (5).
|
|
16
|
+
|
|
17
|
+
**The doctor section is the DEBUG-focused report, not this port's `tan
|
|
18
|
+
doctor` checklist** (tan-cli#357). Until that issue this module substituted
|
|
19
|
+
`doctor_cmd._collect`'s build/flash-readiness list and pinned the exit at
|
|
20
|
+
`SUCCESS`, so a bundle attached to a debug failure carried no debugger state
|
|
21
|
+
at all while automation read `ok: true` next to error-severity issues.
|
|
22
|
+
Measured against the oracle (empty PATH, no SDK): rust rc=4 ok=false
|
|
23
|
+
issues=[`support-bundle.sdkRoot`, `support-bundle.hostPrerequisites`]; the
|
|
24
|
+
port answered rc=0 ok=true with six error issues.
|
|
25
|
+
|
|
26
|
+
[`_debug_doctor_report`] is therefore the port of `tan_core::debug::doctor::
|
|
27
|
+
build_doctor_report` + `tan-cli`'s two appends (`append_host_prerequisites`,
|
|
28
|
+
`append_host_environment`). The base/target-branch checks are written here
|
|
29
|
+
because nothing else in this port produces them -- `support-bundle` is this
|
|
30
|
+
port's ONLY debug-report consumer, the debug half of `tan doctor` still
|
|
31
|
+
being unported (`doctor_cmd.py`'s own module docstring). The five host
|
|
32
|
+
checks are NOT rewritten: they are harvested by name from
|
|
33
|
+
`doctor_cmd.host_environment_checks`, the seam that owns each one, so a
|
|
34
|
+
change there reaches the bundle with no second copy to drift (see
|
|
35
|
+
[`_host_checks_from_doctor`]). `--target-kind`/`--server` now genuinely
|
|
36
|
+
change the report, exactly as they do in the oracle.
|
|
37
|
+
|
|
38
|
+
**tan-cli#441: `host_environment_checks` replaced a call into
|
|
39
|
+
`doctor_cmd._collect`.** Until this fix, harvesting these five checks by
|
|
40
|
+
name meant running `_collect`'s WHOLE build/flash-readiness checklist first
|
|
41
|
+
-- `sdk`/`boardYaml`/`workspace`/`westResolved`/`venvProvenance`/
|
|
42
|
+
`zephyrVersion`/`zephyrWorkspace`/`hostPython`/`pythonFloor`/`west`/
|
|
43
|
+
`zephyrSdk`/`sevenZip`/`setools`/`jlink`/`sdkProvenance`, none of which this
|
|
44
|
+
command ever reported -- including every spawned `west --version`, the JLink
|
|
45
|
+
version-banner probe, and a git provenance shell-out, purely to throw the
|
|
46
|
+
results away. `host_environment_checks` builds ONLY the five kept checks,
|
|
47
|
+
from the same underlying functions `_collect` itself now calls through the
|
|
48
|
+
shared `doctor_cmd._resolve_prerequisites_environment` -- see that seam's own
|
|
49
|
+
docstring for how `_collect` and this command stay byte-identical on the
|
|
50
|
+
checks both still report.
|
|
51
|
+
|
|
52
|
+
**One harvested check is capped, not passed through raw** (tan-cli#374
|
|
53
|
+
finding 1): `doctor_cmd`'s `longPaths` carries a Windows-only `fail` arm
|
|
54
|
+
(tan-cli#306) the oracle cannot reach, and feeding it straight into this
|
|
55
|
+
command's verdict made a first-run customer host (registry `LongPathsEnabled`
|
|
56
|
+
on, no global `.gitconfig`) exit 4 where the oracle exits 0 with no issues,
|
|
57
|
+
on every target/server shape. See [`_demote_long_paths_fail`].
|
|
58
|
+
|
|
59
|
+
REDACTION POLICY (this port's own decision -- the oracle does not redact at
|
|
60
|
+
all; verified: a fresh bundle from a freshly-built `target/debug/tan.exe`
|
|
61
|
+
writes the literal `Home directory has no spaces: C:\\Users\\<realname>`
|
|
62
|
+
straight into the file). Every string value in the WRITTEN FILE -- never the
|
|
63
|
+
stdout envelope, whose `data.outputPath` must stay a real, followable path for
|
|
64
|
+
the caller that just asked for it -- has every literal occurrence of the
|
|
65
|
+
resolved home directory (`%USERPROFILE%` on Windows, `$HOME` elsewhere, in
|
|
66
|
+
both its native and its posix-slash spelling) replaced with the placeholder
|
|
67
|
+
`<home>`. This is deliberately narrow, not a blanket path-scrubber: it targets
|
|
68
|
+
the one concrete PII class this bundle contains today -- the OS account name,
|
|
69
|
+
which rides on almost every absolute path the bundle reports (`workspaceRoot`,
|
|
70
|
+
`sdkRoot`, every trace `outputPath`/command line, and the doctor section's own
|
|
71
|
+
`homePath`-flavoured detail strings) -- while leaving a workspace/SDK root
|
|
72
|
+
OUTSIDE the home directory legible on purpose: a maintainer reading an
|
|
73
|
+
attached bundle needs the real project layout to diagnose a path problem, and
|
|
74
|
+
none of this command's own inputs (tool presence/versions, filesystem facts)
|
|
75
|
+
carry a token or credential to redact beyond the account name. See
|
|
76
|
+
[`_redact`]/[`_home_variants`].
|
|
77
|
+
|
|
78
|
+
Two limits on that replacement, both tan-cli#499 defect 5. It is anchored to a
|
|
79
|
+
PATH BOUNDARY, because a bare `str.replace` rewrote any path the home merely
|
|
80
|
+
prefixed into a silently wrong one (with `HOME=/home/<user>`,
|
|
81
|
+
`/home/<user>-two/alp-sdk` -> `<home>-two/alp-sdk`). And it is REFUSED outright for a home that is a
|
|
82
|
+
filesystem root ([`home_redaction_refusal`]) -- `HOME=/` is what Docker hands a
|
|
83
|
+
uid with no `/etc/passwd` entry, and replacing it shredded every separator in
|
|
84
|
+
the file (measured: exit 0, no warning, 88 substitutions in one bundle). A refusal emits `support-bundle.redaction-skipped` in the envelope
|
|
85
|
+
AND in text mode: the user is about to attach this file believing it was
|
|
86
|
+
scrubbed.
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
from __future__ import annotations
|
|
90
|
+
|
|
91
|
+
import json
|
|
92
|
+
import ntpath
|
|
93
|
+
import os
|
|
94
|
+
import re
|
|
95
|
+
from dataclasses import dataclass, replace
|
|
96
|
+
from typing import Any
|
|
97
|
+
|
|
98
|
+
import typer
|
|
99
|
+
|
|
100
|
+
from tan.commands import doctor_cmd
|
|
101
|
+
from tan.commands.inspect_cmd import (
|
|
102
|
+
ResolvedDebugContext,
|
|
103
|
+
collect_resolved_values,
|
|
104
|
+
resolve_debug_project_context,
|
|
105
|
+
)
|
|
106
|
+
from tan.commands.sdk_cmd import NO_SDK_NEXT_STEPS
|
|
107
|
+
from tan.commands.trace_cmd import (
|
|
108
|
+
TraceTargetError,
|
|
109
|
+
build_trace_decisions,
|
|
110
|
+
resolve_trace_targets,
|
|
111
|
+
)
|
|
112
|
+
from tan.core.sdk_discovery import sdk_resolution_issues
|
|
113
|
+
from tan.core.debug_launch import (
|
|
114
|
+
BAREMETAL_MCU,
|
|
115
|
+
GDBSERVER,
|
|
116
|
+
JLINK,
|
|
117
|
+
NATIVE_HOST,
|
|
118
|
+
OPENOCD,
|
|
119
|
+
PYOCD,
|
|
120
|
+
SERVER_NONE,
|
|
121
|
+
YOCTO_USERSPACE,
|
|
122
|
+
ZEPHYR_MCU,
|
|
123
|
+
DebugConfigError,
|
|
124
|
+
is_server_supported_for_target,
|
|
125
|
+
parse_server_kind,
|
|
126
|
+
parse_target_kind,
|
|
127
|
+
)
|
|
128
|
+
from tan.core.timestamp import generated_at_iso
|
|
129
|
+
from tan.envelope import Envelope, Issue, Project, SdkInfo, emit
|
|
130
|
+
from tan.exit_codes import ExitCode
|
|
131
|
+
from tan.output_format import FORMAT_HELP, OutputFormat, resolve_format
|
|
132
|
+
|
|
133
|
+
#: `data.schemaVersion` for this command's stdout payload, and the bundle
|
|
134
|
+
#: file's own top-level + per-section schema versions (all "1", like the
|
|
135
|
+
#: oracle).
|
|
136
|
+
DATA_SCHEMA_VERSION = "1"
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
# ---------------------------------------------------------------------------
|
|
140
|
+
# Redaction -- see the module docstring's "REDACTION POLICY".
|
|
141
|
+
# ---------------------------------------------------------------------------
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
#: The issue code that says the bundle went out UNREDACTED. Redaction is a
|
|
145
|
+
#: best-effort privacy measure, so refusing it is not an error -- but it must
|
|
146
|
+
#: not be silent either: the user is about to attach this file to a public
|
|
147
|
+
#: issue believing the account name was scrubbed.
|
|
148
|
+
REDACTION_SKIPPED_CODE = "support-bundle.redaction-skipped"
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _resolved_home() -> str:
|
|
152
|
+
"""`%USERPROFILE%` on Windows, `$HOME` elsewhere; `""` when unset."""
|
|
153
|
+
return os.environ.get("USERPROFILE" if os.name == "nt" else "HOME") or ""
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def home_redaction_refusal(home: str) -> str | None:
|
|
157
|
+
"""Why `<home>`-redaction must NOT run for this home directory, or `None`
|
|
158
|
+
when it is safe to run.
|
|
159
|
+
|
|
160
|
+
A home that is a filesystem/drive/UNC ROOT is refused. Docker and OpenShift
|
|
161
|
+
hand a uid with no `/etc/passwd` entry `HOME=/`, and an unanchored
|
|
162
|
+
`str.replace("/", "<home>")` then rewrites EVERY separator in EVERY string
|
|
163
|
+
of the bundle. MEASURED: `HOME=/ tan support-bundle` exited 0, emitted no
|
|
164
|
+
warning, and wrote 88 `<home>` substitutions into one bundle -- every
|
|
165
|
+
separator of every path in it. The shape is
|
|
166
|
+
`workspaceRoot: "<home>srv<home>ci<home>work<home>..."`, ILLUSTRATIVE and
|
|
167
|
+
not a transcript: the run's real paths are elided. The maintainer
|
|
168
|
+
receiving that attachment cannot read a single path -- the bundle exists to
|
|
169
|
+
carry exactly those paths verbatim -- and there is nothing to protect
|
|
170
|
+
anyway, because a root home contains no account name.
|
|
171
|
+
|
|
172
|
+
Pure and taking the home as an argument so the rule is testable without
|
|
173
|
+
mutating the process environment.
|
|
174
|
+
|
|
175
|
+
`ntpath.splitdrive` unconditionally, not `os.path`'s: the same cross-OS
|
|
176
|
+
rule `core.image_bundle.is_plain_filename` settled on. `os.path.splitdrive`
|
|
177
|
+
is a no-op on POSIX, so a Windows drive or UNC root would be judged an
|
|
178
|
+
ordinary directory when this rule is exercised anywhere but Windows, and
|
|
179
|
+
the rule would then be untestable on the host CI runs on.
|
|
180
|
+
"""
|
|
181
|
+
if not home:
|
|
182
|
+
return None
|
|
183
|
+
stripped = ntpath.splitdrive(home)[1].strip("\\/")
|
|
184
|
+
if stripped == "":
|
|
185
|
+
return (
|
|
186
|
+
f'$HOME is the filesystem root ("{home}"), so redacting it would '
|
|
187
|
+
"replace every path separator in the bundle. Written UNREDACTED "
|
|
188
|
+
"instead -- a root home carries no account name to protect, but "
|
|
189
|
+
"check the file before attaching it anywhere public."
|
|
190
|
+
)
|
|
191
|
+
return None
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _home_variants() -> tuple[str, ...]:
|
|
195
|
+
"""The resolved home directory, in both its native and posix-slash
|
|
196
|
+
spelling -- the bundle mixes both (native in doctor detail strings and
|
|
197
|
+
`--destination`-normalised paths, posix in `workspaceRoot`/`sdkRoot`/
|
|
198
|
+
`boardYamlPath`), so redaction has to look for both. Empty when the host
|
|
199
|
+
has neither `USERPROFILE` nor `HOME` set -- redacts nothing rather than
|
|
200
|
+
guessing -- and empty for a home [`home_redaction_refusal`] refuses."""
|
|
201
|
+
home = _resolved_home()
|
|
202
|
+
if not home or home_redaction_refusal(home):
|
|
203
|
+
return ()
|
|
204
|
+
return tuple({home, home.replace("\\", "/")})
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
#: Characters that end a path token in the strings this bundle carries. A home
|
|
208
|
+
#: occurrence must START at one of these (or at the beginning of the string) and
|
|
209
|
+
#: END at one of these, at a path separator, or at the end of the string.
|
|
210
|
+
#:
|
|
211
|
+
#: `/` and `\\` are deliberately ABSENT from the leading set for a home with no
|
|
212
|
+
#: drive. The shape that rule DECIDES is a home directly preceded by a
|
|
213
|
+
#: separator -- `/srv//home/dev/proj`, or `file:///home/dev/proj` -- which names
|
|
214
|
+
#: a different directory and must survive; with `HOME=/home/dev` both are left
|
|
215
|
+
#: alone, and treating a separator as a token start would rewrite the first to
|
|
216
|
+
#: `/srv/<home>/proj`.
|
|
217
|
+
#:
|
|
218
|
+
#: Note what this rule does NOT decide, since an earlier version of this comment
|
|
219
|
+
#: claimed it did: `/srv/home/dev/proj` (single separator) is already safe
|
|
220
|
+
#: because the character before the match is `v`, an ordinary name character
|
|
221
|
+
#: that is in no boundary set. Measured both ways -- it survives even with
|
|
222
|
+
#: separators admitted to the leading set.
|
|
223
|
+
#:
|
|
224
|
+
#: Separators ARE in the trailing set, because `<home>/proj` is exactly the
|
|
225
|
+
#: substitution wanted, and they are added back to the LEADING set for a
|
|
226
|
+
#: drive-anchored home -- see [`_home_pattern`].
|
|
227
|
+
_TOKEN_BREAK = " \t\r\n\"'=,;:()[]{}<>|`"
|
|
228
|
+
|
|
229
|
+
#: Path separators. A leading boundary for a DRIVE-ANCHORED home only.
|
|
230
|
+
_SEPARATORS = "/\\"
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _home_pattern(variant: str) -> re.Pattern[str]:
|
|
234
|
+
"""A path-boundary-anchored matcher for one home spelling.
|
|
235
|
+
|
|
236
|
+
A bare `str.replace` rewrote any path the home merely PREFIXES into a
|
|
237
|
+
silently wrong one: measured with `HOME=/home/<user>`,
|
|
238
|
+
`/home/<user>-two/alp-sdk` was written as `<home>-two/alp-sdk` and
|
|
239
|
+
`/srv/home/<user>-ops/proj` as `/srv<home>-ops/proj` -- register-grade path
|
|
240
|
+
data corrupted in the one artifact that exists to carry it, at `ok: true`.
|
|
241
|
+
|
|
242
|
+
A DRIVE-ANCHORED variant also accepts a separator as its leading boundary.
|
|
243
|
+
`%USERPROFILE%` starts with a drive letter, so Windows' extended-length
|
|
244
|
+
prefix put one in front of it and the account name survived: measured,
|
|
245
|
+
`\\\\?\\C:\\Users\\<name>\\proj` came back UNREDACTED, on the one platform
|
|
246
|
+
whose home directory is the account name. The POSIX exclusion above cannot
|
|
247
|
+
be relaxed, but it does not apply here -- a drive letter re-anchors the path
|
|
248
|
+
absolutely, so no preceding component can make `C:\\Users\\alice` name a
|
|
249
|
+
different directory the way `/srv` + `/home/dev` does.
|
|
250
|
+
"""
|
|
251
|
+
break_class = re.escape(_TOKEN_BREAK)
|
|
252
|
+
drive_anchored = ntpath.splitdrive(variant)[0] != ""
|
|
253
|
+
lead_class = break_class + (re.escape(_SEPARATORS) if drive_anchored else "")
|
|
254
|
+
return re.compile(
|
|
255
|
+
rf"(?<![^{lead_class}])" # string start, or a leading boundary before it
|
|
256
|
+
+ re.escape(variant)
|
|
257
|
+
+ rf"(?=[{re.escape(_SEPARATORS)}]|[{break_class}]|$)", # sep, break, or end
|
|
258
|
+
# A drive-anchored (Windows) home names a case-INSENSITIVE volume: VS Code
|
|
259
|
+
# hands tan `c:\\Users\\Alice` for a `C:\\Users\\Alice` profile, and a
|
|
260
|
+
# case-sensitive match would leak the account name (tan-cli#1483). A POSIX
|
|
261
|
+
# home stays case-sensitive -- `/home/Alice` and `/home/alice` differ there.
|
|
262
|
+
re.IGNORECASE if drive_anchored else 0,
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _redact(value: Any, home_variants: tuple[str, ...]) -> Any:
|
|
267
|
+
"""Recursively replace every path-boundary-anchored `home_variants`
|
|
268
|
+
occurrence in every string this bundle payload carries with `<home>`. Walks
|
|
269
|
+
dicts/lists; every other JSON-safe type (bool/int/float/None) passes through
|
|
270
|
+
unchanged."""
|
|
271
|
+
if isinstance(value, str):
|
|
272
|
+
for variant in home_variants:
|
|
273
|
+
value = _home_pattern(variant).sub("<home>", value)
|
|
274
|
+
return value
|
|
275
|
+
if isinstance(value, dict):
|
|
276
|
+
return {k: _redact(v, home_variants) for k, v in value.items()}
|
|
277
|
+
if isinstance(value, list):
|
|
278
|
+
return [_redact(v, home_variants) for v in value]
|
|
279
|
+
return value
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
# ---------------------------------------------------------------------------
|
|
283
|
+
# Bundle assembly
|
|
284
|
+
# ---------------------------------------------------------------------------
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def _timestamp_for_file(generated_at: str) -> str:
|
|
288
|
+
"""Makes an ISO timestamp filename-safe: `:`/`.` -> `-`. Mirrors the
|
|
289
|
+
oracle's `timestamp_for_file`."""
|
|
290
|
+
return "".join("-" if c in ":." else c for c in generated_at)
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def _create_bundle_trace_decisions(
|
|
294
|
+
context: ResolvedDebugContext, target: str | None, focus: str | None
|
|
295
|
+
) -> list[dict[str, Any]]:
|
|
296
|
+
"""The bundle's own trace section: `Planned` decisions when an SDK root
|
|
297
|
+
resolved, else one `Failed` placeholder -- port of `support_bundle.rs`'s
|
|
298
|
+
`create_bundle_trace_decisions`.
|
|
299
|
+
|
|
300
|
+
Deliberately checks ONLY `context.sdk_root is not None`, never
|
|
301
|
+
`board_yaml_exists` -- measured against the oracle: a project with a
|
|
302
|
+
resolved SDK but a MISSING board.yaml still gets four `Planned` decisions
|
|
303
|
+
here (`decisionCount: 4`), unlike bare `tan trace`, which refuses outright
|
|
304
|
+
on a missing board.yaml. `workspace_root`/`board_yaml_path` need no
|
|
305
|
+
presence check of their own in this port: both are unconditionally
|
|
306
|
+
resolved by [`resolve_debug_project_context`] (see that module's
|
|
307
|
+
docstring), exactly mirroring why the oracle's own three-way `Option`
|
|
308
|
+
match only ever turns on `sdk_root`.
|
|
309
|
+
"""
|
|
310
|
+
if context.sdk_root is None:
|
|
311
|
+
decisions: list[dict[str, Any]] = [
|
|
312
|
+
{
|
|
313
|
+
"key": "generation.targets",
|
|
314
|
+
"outcome": "failed",
|
|
315
|
+
"detail": (
|
|
316
|
+
"Generation targets were not traced because project context "
|
|
317
|
+
"is unresolved."
|
|
318
|
+
),
|
|
319
|
+
}
|
|
320
|
+
]
|
|
321
|
+
else:
|
|
322
|
+
targets = resolve_trace_targets(target) # may raise TraceTargetError
|
|
323
|
+
decisions = build_trace_decisions(
|
|
324
|
+
context.workspace_root,
|
|
325
|
+
context.sdk_root,
|
|
326
|
+
context.board_yaml_path,
|
|
327
|
+
context.python_binary,
|
|
328
|
+
targets,
|
|
329
|
+
None, # the focus-path decision is appended once, below
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
if focus is not None:
|
|
333
|
+
decisions.append(
|
|
334
|
+
{
|
|
335
|
+
"key": f"config.path.{focus}",
|
|
336
|
+
"outcome": "planned",
|
|
337
|
+
"detail": (
|
|
338
|
+
"Path-level trace was requested and captured as part of "
|
|
339
|
+
"bundle metadata."
|
|
340
|
+
),
|
|
341
|
+
}
|
|
342
|
+
)
|
|
343
|
+
return decisions
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
#: The `DebuggerExtensionsState` the standalone binary reports, verbatim from
|
|
347
|
+
#: `crates/tan-cli/src/commands/doctor.rs::standalone_debugger_extensions`.
|
|
348
|
+
#: The three flags are an inherited assumption from the VS Code extension --
|
|
349
|
+
#: only an extension host can enumerate its own marketplace extensions -- and
|
|
350
|
+
#: `observable: false` is what stops anything reading them as facts: every
|
|
351
|
+
#: derived check renders `unknown` instead of claiming "vadimcn.vscode-lldb is
|
|
352
|
+
#: installed." on a headless container (#102).
|
|
353
|
+
STANDALONE_DEBUGGER_EXTENSIONS = {
|
|
354
|
+
"cortexDebug": True,
|
|
355
|
+
"cppTools": True,
|
|
356
|
+
"codeLLDB": True,
|
|
357
|
+
"observable": False,
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
#: Per-server executable candidates, first hit on PATH wins -- port of
|
|
361
|
+
#: `tan_core::debug::context::collect_runtime_capabilities_from_commands`. The
|
|
362
|
+
#: reported value is the COMMAND NAME, not the resolved path: that is what the
|
|
363
|
+
#: oracle puts in the check detail (`.map(|c| (*c).to_string())`).
|
|
364
|
+
_RUNTIME_EXECUTABLES: dict[str, tuple[str, ...]] = {
|
|
365
|
+
JLINK: ("JLinkGDBServerCL", "JLinkGDBServer"),
|
|
366
|
+
OPENOCD: ("openocd",),
|
|
367
|
+
PYOCD: ("pyocd",),
|
|
368
|
+
GDBSERVER: ("gdb", "arm-none-eabi-gdb"),
|
|
369
|
+
SERVER_NONE: ("lldb-dap", "lldb"),
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
#: The host checks the oracle appends to the pure report AFTER building it
|
|
373
|
+
#: (`append_host_prerequisites` then `append_host_environment`), in that
|
|
374
|
+
#: order. `bootstrapManifest` has no oracle counterpart as a CHECK -- Rust
|
|
375
|
+
#: folds a rejected `metadata/bootstrap.json` into `hostPrerequisites`'
|
|
376
|
+
#: own detail via `manifest_error`, this port reports it as a sibling warn --
|
|
377
|
+
#: so it rides here rather than being dropped: a bundle whose prerequisite
|
|
378
|
+
#: list came from tan's fallback instead of the SDK's manifest must say so.
|
|
379
|
+
#: `longPaths` is Windows-only and simply absent elsewhere. Its `fail` status
|
|
380
|
+
#: is demoted before it ever reaches this command's verdict -- see
|
|
381
|
+
#: [`_demote_long_paths_fail`].
|
|
382
|
+
_HOST_CHECK_ORDER = (
|
|
383
|
+
"bootstrapManifest",
|
|
384
|
+
"hostPrerequisites",
|
|
385
|
+
"zephyrSdkAvailableForHost",
|
|
386
|
+
"longPaths",
|
|
387
|
+
"homePath",
|
|
388
|
+
)
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def _demote_long_paths_fail(check: doctor_cmd.Check) -> doctor_cmd.Check:
|
|
392
|
+
"""tan-cli#374 finding 1. tan-cli#306 widened `doctor_cmd.long_paths_check`
|
|
393
|
+
with a Windows-only `fail` arm (registry `LongPathsEnabled` on, git's own
|
|
394
|
+
`core.longpaths` not) that the oracle cannot reach at all: Rust's
|
|
395
|
+
`long_paths_check` (`crates/tan-core/src/host_env.rs:300-330`) takes only
|
|
396
|
+
the registry axis and never returns worse than `Warn` -- confirmed by
|
|
397
|
+
RUNNING the oracle, not by reading it. Every OTHER `longPaths` state
|
|
398
|
+
(`warn`/`pass`) already matches the oracle's own verdict for that state
|
|
399
|
+
and is left alone; only this one, oracle-unreachable combination is
|
|
400
|
+
downgraded.
|
|
401
|
+
|
|
402
|
+
Feeding the undemoted check into this command's verdict was exactly the
|
|
403
|
+
bug: on a first-run customer host (registry on, no global `.gitconfig`,
|
|
404
|
+
so git's own flag is unset) it made `support-bundle` exit 4 -- doctor
|
|
405
|
+
failure -- where the oracle exits 0 with no issues at all, on every
|
|
406
|
+
target/server shape. `tan doctor` KEEPS the fail arm unchanged (that is
|
|
407
|
+
#306's real fix, for the command that actually gates a build and where
|
|
408
|
+
"fail fast on a certain west-update break" is the right call); this
|
|
409
|
+
command only ever attaches a debug snapshot, and its own acceptance bar
|
|
410
|
+
is matching the oracle's axes, which do not include this one.
|
|
411
|
+
|
|
412
|
+
Demoting here (not filtering the check out of the report) keeps the
|
|
413
|
+
written bundle, `doctor.summary`, `nextSteps` and the wire verdict all
|
|
414
|
+
reading the SAME check list -- a maintainer opening the file still sees
|
|
415
|
+
the real git/registry mismatch (the `detail`/`fix` text is untouched),
|
|
416
|
+
just at the severity this command can actually promise the oracle
|
|
417
|
+
agrees with.
|
|
418
|
+
"""
|
|
419
|
+
if check.name == "longPaths" and check.status == "fail":
|
|
420
|
+
return replace(check, status="warn")
|
|
421
|
+
return check
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
def _first_on_path(names: tuple[str, ...]) -> str | None:
|
|
425
|
+
return next((name for name in names if doctor_cmd.on_path(name) is not None), None)
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def _extension_check(name: str, extension_id: str) -> doctor_cmd.Check:
|
|
429
|
+
"""One VS Code extension-presence check, always `unknown` here -- port of
|
|
430
|
+
`doctor.rs::extension_check`'s `None` arm, which is the only arm the
|
|
431
|
+
standalone binary can reach (`observable: false`). Not `warn`: a warning
|
|
432
|
+
says something is wrong, and nothing is -- the question was not askable.
|
|
433
|
+
`unknown` is counted in no summary bucket and raises no issue, so it can
|
|
434
|
+
never move this command's exit code.
|
|
435
|
+
|
|
436
|
+
The dash in `detail` is a real U+2014 EM DASH, not `--` (tan-cli#374
|
|
437
|
+
finding 6): the oracle's own literal
|
|
438
|
+
(`crates/tan-core/src/debug/doctor.rs:132`) uses one, and this string is a
|
|
439
|
+
verbatim port of it -- unlike this file's own prose/comments, which use
|
|
440
|
+
`--` throughout, an emitted user-facing detail string copies its source of
|
|
441
|
+
truth byte for byte."""
|
|
442
|
+
return doctor_cmd.Check(
|
|
443
|
+
name,
|
|
444
|
+
"unknown",
|
|
445
|
+
f"{extension_id}: unknown — the standalone tan binary cannot see "
|
|
446
|
+
"VS Code's installed extensions.",
|
|
447
|
+
scope="host",
|
|
448
|
+
)
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
def _host_checks_from_doctor(context: ResolvedDebugContext) -> list[doctor_cmd.Check]:
|
|
452
|
+
"""The host half of the bundled report, harvested BY NAME from
|
|
453
|
+
`doctor_cmd.host_environment_checks` rather than re-probed here.
|
|
454
|
+
|
|
455
|
+
The oracle appends these with `append_host_prerequisites` /
|
|
456
|
+
`append_host_environment`, both `pub(crate)` in `tan-cli` precisely so
|
|
457
|
+
`support_bundle.rs` can share `doctor.rs`'s copy. This port has the same
|
|
458
|
+
five checks -- `prerequisites_check`, `zephyr_sdk_host_check`,
|
|
459
|
+
`long_paths_check`, `home_path_check`, plus `bootstrapManifest` (see
|
|
460
|
+
`_HOST_CHECK_ORDER`) -- and `doctor_cmd.host_environment_checks` is the
|
|
461
|
+
ONE place that builds them; `doctor_cmd._collect` (`tan doctor`'s own,
|
|
462
|
+
much longer checklist) calls the same underlying
|
|
463
|
+
`_resolve_prerequisites_environment` seam for `bootstrapManifest`/
|
|
464
|
+
`hostPrerequisites`, so this command and `tan doctor` can never build a
|
|
465
|
+
diverging verdict for a check both report.
|
|
466
|
+
|
|
467
|
+
tan-cli#441: this used to call `doctor_cmd._collect` and pick the five
|
|
468
|
+
checks out by name, which meant running `_collect`'s WHOLE
|
|
469
|
+
build/flash-readiness checklist first -- `sdk`/`boardYaml`/`workspace`/
|
|
470
|
+
`westResolved`/`venvProvenance`/`zephyrVersion`/`zephyrWorkspace`/
|
|
471
|
+
`hostPython`/`pythonFloor`/`west`/`zephyrSdk`/`sevenZip`/`setools`/
|
|
472
|
+
`jlink`/`sdkProvenance`, none of which this command ever reported --
|
|
473
|
+
including every spawned `west --version`, the JLink version-banner probe,
|
|
474
|
+
and a git provenance shell-out, purely to throw the results away.
|
|
475
|
+
`host_environment_checks` never builds any of those: no board.yaml or
|
|
476
|
+
project scope is even accepted any more, because none of the five kept
|
|
477
|
+
checks reads either (`sdk_check`/`board_yaml_preflight_check`/
|
|
478
|
+
`libraries_check` are the only `_collect` checks that did, and none of
|
|
479
|
+
the three is one of the five).
|
|
480
|
+
"""
|
|
481
|
+
collected = doctor_cmd.host_environment_checks(
|
|
482
|
+
context.sdk_root,
|
|
483
|
+
workspace_root=context.workspace_root,
|
|
484
|
+
)
|
|
485
|
+
by_name = {check.name: check for check in collected}
|
|
486
|
+
return [
|
|
487
|
+
_demote_long_paths_fail(by_name[name])
|
|
488
|
+
for name in _HOST_CHECK_ORDER
|
|
489
|
+
if name in by_name
|
|
490
|
+
]
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
def _debug_doctor_report(
|
|
494
|
+
context: ResolvedDebugContext,
|
|
495
|
+
project_selected: bool,
|
|
496
|
+
target: str,
|
|
497
|
+
server: str,
|
|
498
|
+
) -> tuple[dict[str, Any], list[doctor_cmd.Check]]:
|
|
499
|
+
"""The bundle's DEBUG-focused doctor section (tan-cli#357) -- port of
|
|
500
|
+
`tan_core::debug::doctor::build_doctor_report` plus `tan-cli`'s two host
|
|
501
|
+
appends. See the module docstring for what it replaced and why.
|
|
502
|
+
|
|
503
|
+
No `serverCompatibility` check: the oracle's builder emits one and then
|
|
504
|
+
short-circuits, but `support_bundle.rs` refuses an unsupported pairing
|
|
505
|
+
with its own `support-bundle.server-compatibility` issue BEFORE building
|
|
506
|
+
any report (`_server_incompatible` here does the same), so that arm is
|
|
507
|
+
unreachable from this command. Verified against the oracle:
|
|
508
|
+
`--target-kind yocto-userspace --server jlink` writes no bundle at all.
|
|
509
|
+
|
|
510
|
+
`workspaceRoot` has no fail arm for the same reason `inspect_cmd`'s
|
|
511
|
+
resolved-values rows have none: `resolve_debug_project_context` always
|
|
512
|
+
resolves a workspace root (cwd, or `--project` joined onto it), so Rust's
|
|
513
|
+
`is_present(&context.workspace_root)` is always true in this port's
|
|
514
|
+
construction. Left as a reported check because the oracle reports it and
|
|
515
|
+
a bundle reader looks for it.
|
|
516
|
+
"""
|
|
517
|
+
checks = [
|
|
518
|
+
doctor_cmd.Check("workspaceRoot", "pass", context.workspace_root, scope="project"),
|
|
519
|
+
doctor_cmd.Check(
|
|
520
|
+
"sdkRoot",
|
|
521
|
+
"pass" if context.sdk_root else "fail",
|
|
522
|
+
context.sdk_root or "No alp-sdk checkout resolved.",
|
|
523
|
+
None
|
|
524
|
+
if context.sdk_root
|
|
525
|
+
# tan-cli#381: the `fix` said "Run `tan sdk switch <path>`", which
|
|
526
|
+
# refuses in this build (`sdk_cmd._run_not_ported`) -- and a support
|
|
527
|
+
# bundle is read by whoever is already stuck, so a fix line naming a
|
|
528
|
+
# command that exits 1 is the worst place for the #305 dead end to
|
|
529
|
+
# come back. `doctor_cmd.sdk_check`'s own wording, shared verbatim.
|
|
530
|
+
else f"Resolve an alp-sdk checkout: {NO_SDK_NEXT_STEPS}.",
|
|
531
|
+
scope="project",
|
|
532
|
+
),
|
|
533
|
+
_board_yaml_check(context, project_selected),
|
|
534
|
+
*_target_checks(target, server),
|
|
535
|
+
*_host_checks_from_doctor(context),
|
|
536
|
+
]
|
|
537
|
+
missing_prerequisites = next(
|
|
538
|
+
(c.missing for c in checks if c.name == "hostPrerequisites"), None
|
|
539
|
+
)
|
|
540
|
+
report = {
|
|
541
|
+
"generatedAt": None, # filled by the caller, which already has the timestamp
|
|
542
|
+
"targetKind": target,
|
|
543
|
+
"server": server,
|
|
544
|
+
"summary": doctor_cmd.summarise(checks),
|
|
545
|
+
"checks": [c.as_dict() for c in checks],
|
|
546
|
+
"nextSteps": doctor_cmd.next_steps(checks),
|
|
547
|
+
"missingPrerequisites": missing_prerequisites,
|
|
548
|
+
}
|
|
549
|
+
return report, checks
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
def _board_yaml_check(
|
|
553
|
+
context: ResolvedDebugContext, project_selected: bool
|
|
554
|
+
) -> doctor_cmd.Check:
|
|
555
|
+
"""Port of `doctor.rs::board_yaml_check` -- the DEBUG report's `boardYaml`,
|
|
556
|
+
which reports the resolved PATH as its detail (this port's build-checklist
|
|
557
|
+
`board_yaml_preflight_check` reports "board.yaml found" instead, and stays
|
|
558
|
+
where it is).
|
|
559
|
+
|
|
560
|
+
A missing `board.yaml` is a hard failure only once the user NAMED a
|
|
561
|
+
project: with neither `--project` nor `--board-yaml` the path is a guess
|
|
562
|
+
at the working directory, and `tan bootstrap` sends every new customer to
|
|
563
|
+
run `tan doctor` from the SDK checkout root, which has no `board.yaml` and
|
|
564
|
+
needs none (#100).
|
|
565
|
+
"""
|
|
566
|
+
if context.board_yaml_exists:
|
|
567
|
+
return doctor_cmd.Check("boardYaml", "pass", context.board_yaml_path, scope="project")
|
|
568
|
+
if project_selected:
|
|
569
|
+
return doctor_cmd.Check(
|
|
570
|
+
"boardYaml",
|
|
571
|
+
"fail",
|
|
572
|
+
context.board_yaml_path,
|
|
573
|
+
"Create board.yaml or pass `--board-yaml <path>`.",
|
|
574
|
+
scope="project",
|
|
575
|
+
)
|
|
576
|
+
return doctor_cmd.Check(
|
|
577
|
+
"boardYaml",
|
|
578
|
+
"warn",
|
|
579
|
+
f"no project selected -- no board.yaml at {context.board_yaml_path}",
|
|
580
|
+
"Select a project with `--project <dir>` (or `--board-yaml <path>`) to check one.",
|
|
581
|
+
scope="project",
|
|
582
|
+
)
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
def _target_checks(target: str, server: str) -> list[doctor_cmd.Check]:
|
|
586
|
+
"""The per-target-kind arm of `build_doctor_report`: one extension check
|
|
587
|
+
plus one tool check, both driven by `--target-kind`/`--server`.
|
|
588
|
+
|
|
589
|
+
`lldb` always passes, with no `fix` (#131): `vadimcn.vscode-lldb` ships
|
|
590
|
+
its own complete LLDB inside the extension directory and never consults
|
|
591
|
+
PATH, so warning that none was found and telling the user to install one
|
|
592
|
+
is a remedy for a tool the product does not need. The resolved executable
|
|
593
|
+
is still REPORTED when present -- that is real information; only the
|
|
594
|
+
verdict and the advice were wrong.
|
|
595
|
+
"""
|
|
596
|
+
if target in (ZEPHYR_MCU, BAREMETAL_MCU):
|
|
597
|
+
found = _first_on_path(_RUNTIME_EXECUTABLES[server])
|
|
598
|
+
return [
|
|
599
|
+
_extension_check("cortexDebugExtension", "marus25.cortex-debug"),
|
|
600
|
+
doctor_cmd.Check(
|
|
601
|
+
f"{server}Backend",
|
|
602
|
+
"pass" if found else "warn",
|
|
603
|
+
found or f"No {server} executable was found on PATH.",
|
|
604
|
+
None if found else f"Install {server} and make sure it is on PATH.",
|
|
605
|
+
scope="host",
|
|
606
|
+
),
|
|
607
|
+
]
|
|
608
|
+
if target == YOCTO_USERSPACE:
|
|
609
|
+
gdb = _first_on_path(_RUNTIME_EXECUTABLES[GDBSERVER])
|
|
610
|
+
return [
|
|
611
|
+
_extension_check("cppToolsExtension", "ms-vscode.cpptools"),
|
|
612
|
+
doctor_cmd.Check(
|
|
613
|
+
"gdb",
|
|
614
|
+
"pass" if gdb else "warn",
|
|
615
|
+
gdb or "No local gdb executable was found on PATH.",
|
|
616
|
+
None if gdb else "Install gdb locally for symbolized remote debugging.",
|
|
617
|
+
scope="host",
|
|
618
|
+
),
|
|
619
|
+
]
|
|
620
|
+
lldb = _first_on_path(_RUNTIME_EXECUTABLES[SERVER_NONE])
|
|
621
|
+
return [
|
|
622
|
+
_extension_check("codeLLDBExtension", "vadimcn.vscode-lldb"),
|
|
623
|
+
doctor_cmd.Check(
|
|
624
|
+
"lldb",
|
|
625
|
+
"pass",
|
|
626
|
+
lldb or "vadimcn.vscode-lldb ships its own LLDB, so none is needed on PATH.",
|
|
627
|
+
scope="host",
|
|
628
|
+
),
|
|
629
|
+
]
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
def _doctor_issues(checks: list[doctor_cmd.Check]) -> list[Issue]:
|
|
633
|
+
"""Warn/fail checks become `support-bundle.<kebab-name>` issues -- port of
|
|
634
|
+
`support_bundle.rs::doctor_checks_to_issues`, with THIS port's own
|
|
635
|
+
`doctor_cmd.Check` shape (`name`/`status`/`detail`) instead of Rust's
|
|
636
|
+
`DoctorCheck`. `unknown` raises nothing: the question was not askable,
|
|
637
|
+
not a problem."""
|
|
638
|
+
return [
|
|
639
|
+
Issue(
|
|
640
|
+
f"support-bundle.{doctor_cmd.kebab_check_name(c.name)}",
|
|
641
|
+
"error" if c.status == "fail" else "warning",
|
|
642
|
+
c.detail,
|
|
643
|
+
)
|
|
644
|
+
for c in checks
|
|
645
|
+
if c.status in ("warn", "fail")
|
|
646
|
+
]
|
|
647
|
+
|
|
648
|
+
|
|
649
|
+
def _write_bundle(
|
|
650
|
+
destination: str | None,
|
|
651
|
+
workspace_root: str,
|
|
652
|
+
generated_at: str,
|
|
653
|
+
payload: dict[str, Any],
|
|
654
|
+
) -> str:
|
|
655
|
+
"""Write the redacted `payload` to a timestamped
|
|
656
|
+
`debug-support-bundle-*.json` file under `destination` (resolved against
|
|
657
|
+
cwd, like the oracle's `normalize_path(cwd.join(dest))`) or
|
|
658
|
+
`<workspace_root>/.alp-support`. Returns the written path."""
|
|
659
|
+
file_name = f"debug-support-bundle-{_timestamp_for_file(generated_at)}.json"
|
|
660
|
+
base_dir = (
|
|
661
|
+
os.path.abspath(destination)
|
|
662
|
+
if destination is not None
|
|
663
|
+
else os.path.join(workspace_root, ".alp-support")
|
|
664
|
+
)
|
|
665
|
+
output_path = os.path.join(base_dir, file_name)
|
|
666
|
+
os.makedirs(base_dir, exist_ok=True)
|
|
667
|
+
redacted = _redact(payload, _home_variants())
|
|
668
|
+
with open(output_path, "w", encoding="utf-8", newline="") as handle:
|
|
669
|
+
json.dump(redacted, handle, indent=2)
|
|
670
|
+
handle.write("\n")
|
|
671
|
+
return output_path
|
|
672
|
+
|
|
673
|
+
|
|
674
|
+
# ---------------------------------------------------------------------------
|
|
675
|
+
# Envelope assembly
|
|
676
|
+
# ---------------------------------------------------------------------------
|
|
677
|
+
|
|
678
|
+
|
|
679
|
+
@dataclass
|
|
680
|
+
class _Outcome:
|
|
681
|
+
exit_code: ExitCode
|
|
682
|
+
data: dict[str, Any]
|
|
683
|
+
project: Project
|
|
684
|
+
sdk: SdkInfo | None
|
|
685
|
+
issues: list[Issue]
|
|
686
|
+
text: list[str]
|
|
687
|
+
#: Whether `--verbose` may append its "use --format json" hint under text
|
|
688
|
+
#: mode -- only the bundle-written path does (`support_bundle_text` in the
|
|
689
|
+
#: oracle); the three failure shapes (`internal_failure`/
|
|
690
|
+
#: `server_incompatible`, plus this port's outer exception guard) have
|
|
691
|
+
#: their own fixed text and never grow a verbose-only line. Verified: `tan
|
|
692
|
+
#: support-bundle --target-kind yocto-userspace --server jlink --verbose`
|
|
693
|
+
#: prints only the one incompatibility line, no hint.
|
|
694
|
+
verbose_hint_eligible: bool = False
|
|
695
|
+
|
|
696
|
+
|
|
697
|
+
def _empty_data(generated_at: str, target: str, server: str) -> dict[str, Any]:
|
|
698
|
+
return {
|
|
699
|
+
"schemaVersion": DATA_SCHEMA_VERSION,
|
|
700
|
+
"generatedAt": generated_at,
|
|
701
|
+
"outputPath": "",
|
|
702
|
+
"targetKind": target,
|
|
703
|
+
"server": server,
|
|
704
|
+
"decisionCount": 0,
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
|
|
708
|
+
def _internal_failure(
|
|
709
|
+
generated_at: str,
|
|
710
|
+
message: str,
|
|
711
|
+
target: str,
|
|
712
|
+
server: str,
|
|
713
|
+
sdk: SdkInfo | None,
|
|
714
|
+
*,
|
|
715
|
+
broken_project_pin: str | None = None,
|
|
716
|
+
sdk_tier: str = "none",
|
|
717
|
+
foreign_global_default_for: str | None = None,
|
|
718
|
+
) -> _Outcome:
|
|
719
|
+
# tan-cli#478 MAJOR 1: this early-return path used to drop the
|
|
720
|
+
# SDK-resolution pair entirely -- exactly the customer who most needs it,
|
|
721
|
+
# since it fires on a machine where something has already gone wrong.
|
|
722
|
+
# `sdk_resolution_issues` is `[]` when neither fires, so callers that
|
|
723
|
+
# have not resolved a project context yet (the outer exception guard)
|
|
724
|
+
# can pass the defaults and get the prior behaviour unchanged.
|
|
725
|
+
issues = sdk_resolution_issues(broken_project_pin, sdk_tier, foreign_global_default_for)
|
|
726
|
+
issues.append(Issue("support-bundle.internal-failure", "error", message))
|
|
727
|
+
return _Outcome(
|
|
728
|
+
exit_code=ExitCode.INTERNAL_FAILURE,
|
|
729
|
+
data=_empty_data(generated_at, target, server),
|
|
730
|
+
project=Project(root=None, board_yaml=None),
|
|
731
|
+
sdk=sdk,
|
|
732
|
+
issues=issues,
|
|
733
|
+
text=["support-bundle: internal failure", message],
|
|
734
|
+
)
|
|
735
|
+
|
|
736
|
+
|
|
737
|
+
def _server_incompatible(
|
|
738
|
+
generated_at: str,
|
|
739
|
+
target: str,
|
|
740
|
+
server: str,
|
|
741
|
+
sdk: SdkInfo | None,
|
|
742
|
+
*,
|
|
743
|
+
broken_project_pin: str | None = None,
|
|
744
|
+
sdk_tier: str = "none",
|
|
745
|
+
foreign_global_default_for: str | None = None,
|
|
746
|
+
) -> _Outcome:
|
|
747
|
+
# tan-cli#478 MAJOR 1: same fix as `_internal_failure` above.
|
|
748
|
+
message = f"Server '{server}' is not supported for target '{target}'."
|
|
749
|
+
issues = sdk_resolution_issues(broken_project_pin, sdk_tier, foreign_global_default_for)
|
|
750
|
+
issues.append(Issue("support-bundle.server-compatibility", "error", message))
|
|
751
|
+
return _Outcome(
|
|
752
|
+
exit_code=ExitCode.DOCTOR_FAILURE,
|
|
753
|
+
data=_empty_data(generated_at, target, server),
|
|
754
|
+
project=Project(root=None, board_yaml=None),
|
|
755
|
+
sdk=sdk,
|
|
756
|
+
issues=issues,
|
|
757
|
+
text=[f"support-bundle: server '{server}' is not supported for target '{target}'."],
|
|
758
|
+
)
|
|
759
|
+
|
|
760
|
+
|
|
761
|
+
def _run(
|
|
762
|
+
*,
|
|
763
|
+
project_arg: str | None,
|
|
764
|
+
board_yaml_arg: str | None,
|
|
765
|
+
sdk_root_arg: str | None,
|
|
766
|
+
target_kind_arg: str | None,
|
|
767
|
+
server_arg: str | None,
|
|
768
|
+
path_arg: str | None,
|
|
769
|
+
target_arg: str | None,
|
|
770
|
+
destination_arg: str | None,
|
|
771
|
+
) -> _Outcome:
|
|
772
|
+
"""The whole command as a pure-ish computation (project/SDK resolution and
|
|
773
|
+
the bundle-file WRITE are the only IO) returning one outcome. Mirrors
|
|
774
|
+
`debug_config_cmd._run`'s split: nothing here emits or exits, so the
|
|
775
|
+
caller's exception guard can wrap this call without swallowing
|
|
776
|
+
`typer.Exit`."""
|
|
777
|
+
generated_at = generated_at_iso(millis=True)
|
|
778
|
+
context = resolve_debug_project_context(project_arg, board_yaml_arg, sdk_root_arg)
|
|
779
|
+
|
|
780
|
+
try:
|
|
781
|
+
target = parse_target_kind(target_kind_arg)
|
|
782
|
+
server = parse_server_kind(server_arg)
|
|
783
|
+
except DebugConfigError as err:
|
|
784
|
+
return _internal_failure(
|
|
785
|
+
generated_at,
|
|
786
|
+
str(err),
|
|
787
|
+
NATIVE_HOST,
|
|
788
|
+
SERVER_NONE,
|
|
789
|
+
context.sdk,
|
|
790
|
+
broken_project_pin=context.broken_project_pin,
|
|
791
|
+
sdk_tier=context.sdk_tier,
|
|
792
|
+
foreign_global_default_for=context.foreign_global_default_for,
|
|
793
|
+
)
|
|
794
|
+
|
|
795
|
+
if not is_server_supported_for_target(target, server):
|
|
796
|
+
return _server_incompatible(
|
|
797
|
+
generated_at,
|
|
798
|
+
target,
|
|
799
|
+
server,
|
|
800
|
+
context.sdk,
|
|
801
|
+
broken_project_pin=context.broken_project_pin,
|
|
802
|
+
sdk_tier=context.sdk_tier,
|
|
803
|
+
foreign_global_default_for=context.foreign_global_default_for,
|
|
804
|
+
)
|
|
805
|
+
|
|
806
|
+
try:
|
|
807
|
+
decisions = _create_bundle_trace_decisions(context, target_arg, path_arg)
|
|
808
|
+
except TraceTargetError as err:
|
|
809
|
+
return _internal_failure(
|
|
810
|
+
generated_at,
|
|
811
|
+
str(err),
|
|
812
|
+
target,
|
|
813
|
+
server,
|
|
814
|
+
context.sdk,
|
|
815
|
+
broken_project_pin=context.broken_project_pin,
|
|
816
|
+
sdk_tier=context.sdk_tier,
|
|
817
|
+
foreign_global_default_for=context.foreign_global_default_for,
|
|
818
|
+
)
|
|
819
|
+
|
|
820
|
+
# Port of `doctor.rs::project_selected`: `--project`/`--board-yaml` are the
|
|
821
|
+
# whole selection surface, so with neither given the resolved board.yaml
|
|
822
|
+
# path is a guess at the cwd, not a request.
|
|
823
|
+
project_selected = any(
|
|
824
|
+
(arg or "").strip() for arg in (project_arg, board_yaml_arg)
|
|
825
|
+
)
|
|
826
|
+
doctor_report, checks = _debug_doctor_report(context, project_selected, target, server)
|
|
827
|
+
doctor_report["generatedAt"] = generated_at
|
|
828
|
+
|
|
829
|
+
notes = [
|
|
830
|
+
f"targetKind={target}",
|
|
831
|
+
f"server={server}",
|
|
832
|
+
f"workspaceRoot={context.workspace_root}",
|
|
833
|
+
]
|
|
834
|
+
payload = {
|
|
835
|
+
"schemaVersion": DATA_SCHEMA_VERSION,
|
|
836
|
+
"generatedAt": generated_at,
|
|
837
|
+
"inspect": {
|
|
838
|
+
"schemaVersion": DATA_SCHEMA_VERSION,
|
|
839
|
+
"generatedAt": generated_at,
|
|
840
|
+
# The oracle's `DebugWorkspaceContext`, key for key (tan-cli#357).
|
|
841
|
+
# `projectSelected`/`debuggerExtensions` are NOT fabricated: the
|
|
842
|
+
# first is derived from this invocation's own flags (see
|
|
843
|
+
# `project_selected` above, and `doctor.rs::project_selected`), the
|
|
844
|
+
# second is the standalone binary's own declared, self-describing
|
|
845
|
+
# state -- `observable: false` says in the file itself that the
|
|
846
|
+
# three flags were never probed. Omitting them cost the bundle the
|
|
847
|
+
# two facts that explain WHY its `boardYaml` and extension checks
|
|
848
|
+
# read the way they do, in the artifact a debug failure gets
|
|
849
|
+
# attached to.
|
|
850
|
+
"context": {
|
|
851
|
+
"generatedAt": generated_at,
|
|
852
|
+
"workspaceRoot": context.workspace_root,
|
|
853
|
+
"sdkRoot": context.sdk_root,
|
|
854
|
+
"boardYamlPath": context.board_yaml_path,
|
|
855
|
+
"westCwd": context.west_cwd,
|
|
856
|
+
"pythonBinary": context.python_binary,
|
|
857
|
+
"boardYamlExists": context.board_yaml_exists,
|
|
858
|
+
"projectSelected": project_selected,
|
|
859
|
+
"debuggerExtensions": STANDALONE_DEBUGGER_EXTENSIONS,
|
|
860
|
+
},
|
|
861
|
+
"resolvedValues": collect_resolved_values(context),
|
|
862
|
+
},
|
|
863
|
+
"trace": {
|
|
864
|
+
"schemaVersion": DATA_SCHEMA_VERSION,
|
|
865
|
+
"generatedAt": generated_at,
|
|
866
|
+
"workflow": "cli.support-bundle",
|
|
867
|
+
"decisions": decisions,
|
|
868
|
+
},
|
|
869
|
+
"doctor": doctor_report,
|
|
870
|
+
"notes": notes,
|
|
871
|
+
# tan-cli#478's headline requirement, and the item review called worth
|
|
872
|
+
# fixing FIRST: the FILE has to carry it, not just the stdout envelope.
|
|
873
|
+
# An earlier revision computed these AFTER `_write_bundle` below, so
|
|
874
|
+
# the bundle a user attaches to a bug report -- the whole point of the
|
|
875
|
+
# command -- still answered False to the issue's own repro:
|
|
876
|
+
# 'global-default-foreign-project' in open(BUNDLE).read()
|
|
877
|
+
# Its own `doctor` section could not have supplied it either: that set
|
|
878
|
+
# keeps host checks only (tan-cli#441), 8 where a standalone doctor
|
|
879
|
+
# emits ~17.
|
|
880
|
+
"sdkResolution": [
|
|
881
|
+
{"code": issue.code, "severity": issue.severity, "message": issue.message}
|
|
882
|
+
for issue in sdk_resolution_issues(
|
|
883
|
+
context.broken_project_pin,
|
|
884
|
+
context.sdk_tier,
|
|
885
|
+
context.foreign_global_default_for,
|
|
886
|
+
)
|
|
887
|
+
],
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
try:
|
|
891
|
+
output_path = _write_bundle(destination_arg, context.workspace_root, generated_at, payload)
|
|
892
|
+
except OSError as err:
|
|
893
|
+
return _internal_failure(
|
|
894
|
+
generated_at,
|
|
895
|
+
str(err),
|
|
896
|
+
target,
|
|
897
|
+
server,
|
|
898
|
+
context.sdk,
|
|
899
|
+
broken_project_pin=context.broken_project_pin,
|
|
900
|
+
sdk_tier=context.sdk_tier,
|
|
901
|
+
foreign_global_default_for=context.foreign_global_default_for,
|
|
902
|
+
)
|
|
903
|
+
|
|
904
|
+
# tan-cli#357: the doctor summary IS this command's verdict, exactly as in
|
|
905
|
+
# the oracle (`if doctor.summary.fail > 0 { ExitCode::DoctorFailure }`).
|
|
906
|
+
# `exit_code_for` is `any(status == "fail")` over the same list, so it is
|
|
907
|
+
# that rule spelled in this port's vocabulary -- and it keeps the CLI-wide
|
|
908
|
+
# invariant `process exit code == envelope.exitCode == (not ok)` intact,
|
|
909
|
+
# which the previous hardcoded SUCCESS broke: automation saw `ok: true`
|
|
910
|
+
# and exit 0 beside error-severity issues in the same envelope. The bundle
|
|
911
|
+
# file is still written on the failing path -- it is what the user
|
|
912
|
+
# attaches, and a doctor failure is the reason they are attaching it.
|
|
913
|
+
# tan-cli#478: the SDK-resolution pair FIRST, then the doctor rollup.
|
|
914
|
+
# This bundle is what a user sends to explain a broken machine, and it
|
|
915
|
+
# was the only place the foreign-default warning never appeared. The
|
|
916
|
+
# embedded doctor set would not have covered it either -- it keeps host
|
|
917
|
+
# checks only (tan-cli#441): 8, where a standalone doctor emitted ~17.
|
|
918
|
+
issues = sdk_resolution_issues(
|
|
919
|
+
context.broken_project_pin, context.sdk_tier, context.foreign_global_default_for
|
|
920
|
+
)
|
|
921
|
+
# tan-cli#499: a bundle that went out UNREDACTED must say so. `_redact` is
|
|
922
|
+
# silent by construction (it returns a payload either way), so the one
|
|
923
|
+
# condition under which it is skipped wholesale is reported here, beside
|
|
924
|
+
# the path the user is about to attach.
|
|
925
|
+
refusal = home_redaction_refusal(_resolved_home())
|
|
926
|
+
if refusal is not None:
|
|
927
|
+
issues.append(Issue(REDACTION_SKIPPED_CODE, "warning", refusal))
|
|
928
|
+
issues.extend(_doctor_issues(checks))
|
|
929
|
+
exit_code = doctor_cmd.exit_code_for(checks)
|
|
930
|
+
|
|
931
|
+
data = {
|
|
932
|
+
"schemaVersion": DATA_SCHEMA_VERSION,
|
|
933
|
+
"generatedAt": generated_at,
|
|
934
|
+
"outputPath": output_path,
|
|
935
|
+
"targetKind": target,
|
|
936
|
+
"server": server,
|
|
937
|
+
"decisionCount": len(decisions),
|
|
938
|
+
}
|
|
939
|
+
text = [
|
|
940
|
+
f"support-bundle: exported {output_path}",
|
|
941
|
+
f"support-bundle: trace decisions={len(decisions)}",
|
|
942
|
+
]
|
|
943
|
+
return _Outcome(
|
|
944
|
+
exit_code=exit_code,
|
|
945
|
+
data=data,
|
|
946
|
+
project=context.project,
|
|
947
|
+
sdk=context.sdk,
|
|
948
|
+
issues=issues,
|
|
949
|
+
text=text,
|
|
950
|
+
verbose_hint_eligible=True,
|
|
951
|
+
)
|
|
952
|
+
|
|
953
|
+
|
|
954
|
+
def support_bundle(
|
|
955
|
+
ctx: typer.Context,
|
|
956
|
+
project: str = typer.Option(
|
|
957
|
+
None, "--project", metavar="PATH", help="Project root (defaults to current directory)."
|
|
958
|
+
),
|
|
959
|
+
target_kind: str = typer.Option(
|
|
960
|
+
None,
|
|
961
|
+
"--target-kind",
|
|
962
|
+
metavar="KIND",
|
|
963
|
+
help="Debug target class (zephyr-mcu, baremetal-mcu, yocto-userspace, native-host).",
|
|
964
|
+
),
|
|
965
|
+
board_yaml: str = typer.Option(
|
|
966
|
+
None,
|
|
967
|
+
"--board-yaml",
|
|
968
|
+
metavar="PATH",
|
|
969
|
+
help="Explicit board.yaml path (overrides project resolution).",
|
|
970
|
+
),
|
|
971
|
+
server: str = typer.Option(
|
|
972
|
+
None,
|
|
973
|
+
"--server",
|
|
974
|
+
metavar="SERVER",
|
|
975
|
+
help="Debug server backend (jlink, openocd, pyocd, gdbserver, none).",
|
|
976
|
+
),
|
|
977
|
+
path: str = typer.Option(
|
|
978
|
+
None, "--path", metavar="PATH", help="Limit generation tracing to this config key path."
|
|
979
|
+
),
|
|
980
|
+
sdk_root: str = typer.Option(
|
|
981
|
+
None, "--sdk-root", metavar="PATH", help="alp-sdk checkout root."
|
|
982
|
+
),
|
|
983
|
+
destination: str = typer.Option(
|
|
984
|
+
None,
|
|
985
|
+
"--destination",
|
|
986
|
+
metavar="DESTINATION",
|
|
987
|
+
help="Output directory for the bundle (default: <workspace>/.alp-support).",
|
|
988
|
+
),
|
|
989
|
+
target: str = typer.Option(
|
|
990
|
+
None,
|
|
991
|
+
"--target",
|
|
992
|
+
metavar="EMIT",
|
|
993
|
+
help="Generation target (e.g. zephyr-conf, dts-overlay, cmake-args, yocto-conf).",
|
|
994
|
+
),
|
|
995
|
+
output_format: OutputFormat = typer.Option(None, "--format", help=FORMAT_HELP),
|
|
996
|
+
verbose: bool = typer.Option(
|
|
997
|
+
False, "--verbose", help="Emit additional diagnostic detail."
|
|
998
|
+
),
|
|
999
|
+
quiet: bool = typer.Option(False, "--quiet", hidden=True),
|
|
1000
|
+
no_color: bool = typer.Option(False, "--no-color", hidden=True),
|
|
1001
|
+
non_interactive: bool = typer.Option(False, "--non-interactive", hidden=True),
|
|
1002
|
+
ci: bool = typer.Option(False, "--ci", hidden=True),
|
|
1003
|
+
all_targets: bool = typer.Option(False, "--all", hidden=True),
|
|
1004
|
+
) -> None:
|
|
1005
|
+
"""Export a diagnostic support bundle (inspect + trace + doctor).
|
|
1006
|
+
|
|
1007
|
+
`--all` is accepted and ignored, same as `tan trace`. `--quiet`/
|
|
1008
|
+
`--no-color`/`--non-interactive`/`--ci` are `global = true` clap options
|
|
1009
|
+
`support_bundle.rs` never reads.
|
|
1010
|
+
"""
|
|
1011
|
+
del quiet, no_color, non_interactive, ci, all_targets
|
|
1012
|
+
resolved_format = resolve_format(output_format, ctx.obj, choices=OutputFormat)
|
|
1013
|
+
json_mode = resolved_format == "json"
|
|
1014
|
+
|
|
1015
|
+
try:
|
|
1016
|
+
outcome = _run(
|
|
1017
|
+
project_arg=project,
|
|
1018
|
+
board_yaml_arg=board_yaml,
|
|
1019
|
+
sdk_root_arg=sdk_root,
|
|
1020
|
+
target_kind_arg=target_kind,
|
|
1021
|
+
server_arg=server,
|
|
1022
|
+
path_arg=path,
|
|
1023
|
+
target_arg=target,
|
|
1024
|
+
destination_arg=destination,
|
|
1025
|
+
)
|
|
1026
|
+
except Exception as err: # noqa: BLE001 -- see debug_config_cmd's identical guard
|
|
1027
|
+
outcome = _internal_failure(
|
|
1028
|
+
generated_at_iso(millis=True),
|
|
1029
|
+
f"support-bundle failed unexpectedly: {err.__class__.__name__}: {err}",
|
|
1030
|
+
NATIVE_HOST,
|
|
1031
|
+
SERVER_NONE,
|
|
1032
|
+
None,
|
|
1033
|
+
)
|
|
1034
|
+
|
|
1035
|
+
if not json_mode:
|
|
1036
|
+
# tan-cli#478 review finding 6: the bundle FILE and the JSON envelope
|
|
1037
|
+
# both carry `sdkResolution`/the foreign-default pair now, but
|
|
1038
|
+
# `outcome.text` (every `_Outcome` constructor above) never did --
|
|
1039
|
+
# the default `tan support-bundle` with no `--format` stayed silent.
|
|
1040
|
+
# Read off `outcome.issues`, which every `_Outcome` constructor above
|
|
1041
|
+
# now builds from `context`'s own carried
|
|
1042
|
+
# `broken_project_pin`/`sdk_tier`/`foreign_global_default_for` --
|
|
1043
|
+
# including the three early-return failure paths. Re-deriving here
|
|
1044
|
+
# from `outcome.sdk` instead would be a second copy of the same
|
|
1045
|
+
# decision in the one command whose whole purpose is explaining a
|
|
1046
|
+
# broken machine; this only ever re-prints what is already there.
|
|
1047
|
+
#
|
|
1048
|
+
# tan-cli#499 adds `support-bundle.redaction-skipped` to the same
|
|
1049
|
+
# loop: the warning that the attached file was NOT scrubbed is
|
|
1050
|
+
# useless in the one mode the user reads, and text is the default.
|
|
1051
|
+
for issue in outcome.issues:
|
|
1052
|
+
if issue.code.startswith("sdk.") or issue.code == REDACTION_SKIPPED_CODE:
|
|
1053
|
+
typer.echo(issue.message, err=True)
|
|
1054
|
+
for line in outcome.text:
|
|
1055
|
+
typer.echo(line, err=True)
|
|
1056
|
+
if verbose and outcome.verbose_hint_eligible:
|
|
1057
|
+
typer.echo(
|
|
1058
|
+
"support-bundle: include --format json for machine-readable envelopes.",
|
|
1059
|
+
err=True,
|
|
1060
|
+
)
|
|
1061
|
+
|
|
1062
|
+
if json_mode:
|
|
1063
|
+
emit(
|
|
1064
|
+
Envelope(
|
|
1065
|
+
"support-bundle",
|
|
1066
|
+
outcome.project,
|
|
1067
|
+
outcome.data,
|
|
1068
|
+
outcome.issues,
|
|
1069
|
+
outcome.exit_code,
|
|
1070
|
+
sdk=outcome.sdk,
|
|
1071
|
+
)
|
|
1072
|
+
)
|
|
1073
|
+
raise typer.Exit(int(outcome.exit_code))
|