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,415 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""Render a host-native filesystem path as a valid URI reference
|
|
3
|
+
(tan-cli#1097).
|
|
4
|
+
|
|
5
|
+
`validate`'s `--format diagnostic-v1` (LSP `uri`) and `--format sarif`
|
|
6
|
+
(`artifactLocation.uri`) both carry a field that is a URI reference by spec,
|
|
7
|
+
not a filesystem path -- SARIF 2.1.0 says so explicitly for
|
|
8
|
+
`artifactLocation.uri`, and LSP diagnostics are keyed by document URI, which
|
|
9
|
+
an editor compares against the URI of an open buffer. Both sites used to emit
|
|
10
|
+
`board_path` bare. On Windows that renders `C:\\w\\proj\\board.yaml`: the
|
|
11
|
+
drive-letter colon and the backslashes are not valid in that position, and
|
|
12
|
+
the string carries no scheme, so neither consumer can resolve it -- silently,
|
|
13
|
+
with no crash or error, exactly the class of defect tan-cli#1073/#1077/#1082/
|
|
14
|
+
#1084 already closed elsewhere in this repo.
|
|
15
|
+
|
|
16
|
+
This is a RENDERING concern of those two exporters, not a resolution one, so
|
|
17
|
+
it lives here rather than in `tan.core.board_context.resolve_board_path`
|
|
18
|
+
(which PR #1090 built to answer "where is the file", with a deliberate
|
|
19
|
+
separator-follows-root rule) or in `validate_cmd.py` itself. Only the two
|
|
20
|
+
`uri` fields are meant to go through [`path_to_uri_reference`]:
|
|
21
|
+
`data.boardYamlPath`, the `--input` argv passed to the SDK validator
|
|
22
|
+
subprocess, and every other `Path()` consumer in this repo stay host-native
|
|
23
|
+
on purpose -- `tests/commands/test_validate_command.py`'s
|
|
24
|
+
`test_absolute_board_yaml_uri_is_a_file_uri_but_boardyamlpath_stays_host_
|
|
25
|
+
native` asserts that split so a future "tidy this up" sweep does not unify
|
|
26
|
+
the two contracts (NOT this module's own test file -- that file proves
|
|
27
|
+
[`path_to_uri_reference`] in isolation and has no `boardYamlPath` of its own
|
|
28
|
+
to assert against).
|
|
29
|
+
|
|
30
|
+
## The relative case is left relative -- deliberately, and scoped narrowly
|
|
31
|
+
|
|
32
|
+
A relative `path` (`resolve_board_path`'s own default, `"./board.yaml"`) is
|
|
33
|
+
returned as a relative reference rather than absolutised. It is already a
|
|
34
|
+
legal relative URI reference per RFC 3986 SS4.2 -- SARIF 2.1.0's
|
|
35
|
+
`artifactLocation.uri` accepts one (`format: uri-reference`). alp-sdk's
|
|
36
|
+
`metadata/schemas/diagnostic-v1.schema.json` (NOT this repo's -- it lives in
|
|
37
|
+
alp-sdk, `git ls-files` here has zero hits for it) types the analogous field
|
|
38
|
+
as a bare `"type": "string"` and accepts a raw filesystem path too, so it
|
|
39
|
+
constrains nothing either way and is not cited as support for staying
|
|
40
|
+
relative. Absolutising `path` here would (a) move the pre-existing pinned
|
|
41
|
+
golden (`test_validate_command.py`'s `"./board.yaml"` pins) and (b) bake the
|
|
42
|
+
process's CWD into an otherwise portable document.
|
|
43
|
+
|
|
44
|
+
**This module used to make NO claim about how a relative reference gets
|
|
45
|
+
resolved -- tan-cli#1117 closes that gap.** An earlier version of this
|
|
46
|
+
docstring (tan-cli#1097 review round 1) tried the same thing by having
|
|
47
|
+
`validate_cmd._sarif_document` declare a SARIF `originalUriBaseIds`/
|
|
48
|
+
`uriBaseId` pair -- round 2 review found that addition declared a base that
|
|
49
|
+
did not actually resolve the reference in the default case (an anchoring
|
|
50
|
+
mismatch between `root` and the CWD `board_path` is actually relative to,
|
|
51
|
+
compounded by a missing trailing slash), and reached that wrong answer
|
|
52
|
+
through an unguarded `Path.resolve()` that could raise on a caller-supplied
|
|
53
|
+
`--project` containing a symlink loop, crashing the command it touched.
|
|
54
|
+
Declaring an authoritatively WRONG base is worse than declaring none --
|
|
55
|
+
round 1's undefined base at least let a spec-conformant consumer fall back to
|
|
56
|
+
its own CWD and succeed. That work was reverted rather than patched a third
|
|
57
|
+
time in place; tan-cli#1117 was filed with both measurements (the wrong-base
|
|
58
|
+
`urljoin` result and the `Path.resolve(strict=False)` symlink-loop crash) so
|
|
59
|
+
whoever picked it up next would not rediscover either.
|
|
60
|
+
|
|
61
|
+
[`cwd_base_uri`] is that pick-up: it anchors on `Path.cwd()` -- never
|
|
62
|
+
`root`, which is what produced the anchoring mismatch -- and never resolves
|
|
63
|
+
a caller-supplied path, so the symlink-loop crash class cannot recur here.
|
|
64
|
+
`validate_cmd._sarif_document` wires it into `originalUriBaseIds`, gated on
|
|
65
|
+
[`is_absolute_path_reference`] so an absolute `--board-yaml` reference (which
|
|
66
|
+
already resolves on its own) never carries a `uriBaseId` SARIF 2.1.0 SS3.4.4
|
|
67
|
+
says it must not.
|
|
68
|
+
"""
|
|
69
|
+
from __future__ import annotations
|
|
70
|
+
|
|
71
|
+
import os
|
|
72
|
+
import re
|
|
73
|
+
from pathlib import Path, PurePosixPath, PureWindowsPath
|
|
74
|
+
from urllib.parse import quote, quote_from_bytes
|
|
75
|
+
|
|
76
|
+
#: A Windows drive letter at the string's own start (`C:...`, no backslash
|
|
77
|
+
#: required yet) -- `resolve_board_path` never emits this bare, but
|
|
78
|
+
#: `--board-yaml` is caller-supplied and this oracle should not miss it.
|
|
79
|
+
#: Review round 1 measured this constant survives a mutation that deletes
|
|
80
|
+
#: it (`_is_windows_spelled` collapsing to `"\\" in path` alone): its only
|
|
81
|
+
#: DISTINCT input is a forward-slash-spelled drive path (`"c:/w/proj/
|
|
82
|
+
#: board.yaml"` -- a colon with no backslash anywhere), which the mutant
|
|
83
|
+
#: renders unchanged, i.e. with a first-segment colon a URI-reference
|
|
84
|
+
#: consumer parses as a scheme. `test_a_forward_slash_spelled_drive_path_
|
|
85
|
+
#: is_still_judged_windows` pins that input; see this module's own test
|
|
86
|
+
#: file for the per-branch (not whole-function) mutation that catches it.
|
|
87
|
+
#:
|
|
88
|
+
#: The leading `^` here is REDUNDANT with `.match()` (`_is_windows_spelled`
|
|
89
|
+
#: below uses `.match()`, not `.search()`) -- `re.Pattern.match()` already
|
|
90
|
+
#: only tries at the string's own start regardless of `^`, so removing `^`
|
|
91
|
+
#: alone changes nothing for any input (round 1 review round 2 measured
|
|
92
|
+
#: this directly: un-anchoring the compiled pattern is behaviourally
|
|
93
|
+
#: indistinguishable from the original under `.match()` for every input
|
|
94
|
+
#: tried). A genuine misclassification of a mid-string colon (e.g.
|
|
95
|
+
#: `"sub/board:1.yaml"`) needs BOTH the anchor removed from the pattern AND
|
|
96
|
+
#: the call switched to `.search()` at once -- this property is doubly
|
|
97
|
+
#: defended, not fragile, and `test_a_colon_later_in_a_relative_path_does_
|
|
98
|
+
#: not_misclassify_it_as_windows` (this module's test file) states it
|
|
99
|
+
#: directly rather than chasing a single-line mutation that does not exist.
|
|
100
|
+
_WINDOWS_DRIVE_RE = re.compile(r"^[A-Za-z]:")
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _is_windows_spelled(path: str) -> bool:
|
|
104
|
+
"""Whether `path` is WINDOWS-spelled, judged from the string itself
|
|
105
|
+
rather than `os.name` (tan-cli#1089/#1090's own rule): a Windows-spelled
|
|
106
|
+
path reaches [`path_to_uri_reference`] on EVERY host, not only on the
|
|
107
|
+
`windows-latest` leg, so the oracle (`PureWindowsPath` vs
|
|
108
|
+
`PurePosixPath`) must come from the path's own spelling and never from
|
|
109
|
+
the running host's. Reading `os.name` would make the ubuntu and macos
|
|
110
|
+
legs answer the POSIX question about a Windows path -- which is the same
|
|
111
|
+
failure [`_absolute_path_to_file_uri`] below records for `Path.as_uri()`,
|
|
112
|
+
and it does not disappear on a Windows runner, it MOVES.
|
|
113
|
+
|
|
114
|
+
An earlier version of this docstring justified the rule with "this repo
|
|
115
|
+
has no Windows host". That is false and is corrected repo-wide in
|
|
116
|
+
tan-cli#1153: `parity.yml`'s `python-tests-shard` job runs this suite on
|
|
117
|
+
`windows-latest` (its matrix `os:` carries all three platforms, with
|
|
118
|
+
`runs-on: ${{ matrix.os }}`) on every `pull_request`. The rule above is
|
|
119
|
+
right for its own reason, not for that one.
|
|
120
|
+
|
|
121
|
+
A LEADING `/` is decided POSIX before any later backslash is even
|
|
122
|
+
consulted (review round 1 MAJOR 1, a real regression this fixed): a
|
|
123
|
+
backslash is a perfectly legal character inside a POSIX filename, e.g.
|
|
124
|
+
`/tmp/proj/we\\ird.yaml`. The pre-fix version of this function read
|
|
125
|
+
`"\\" in path` unconditionally and misrouted that exact path to the
|
|
126
|
+
Windows branch, which does not consider it absolute (`PureWindowsPath(
|
|
127
|
+
"/tmp/proj/we\\ird.yaml").is_absolute()` is `False` -- pathlib requires
|
|
128
|
+
a drive too), so it fell to the RELATIVE Windows arm and had its
|
|
129
|
+
backslash swapped for a slash: `/tmp/proj/we/ird.yaml`, silently naming
|
|
130
|
+
a DIFFERENT file than the one on disk, and losing its `file:` scheme
|
|
131
|
+
into the bargain. A leading `/` never appears at the start of a
|
|
132
|
+
Windows-spelled string this repo's own resolver produces (a Windows
|
|
133
|
+
drive path starts with a letter, a UNC path starts with `\\\\`), so this
|
|
134
|
+
is a safe, targeted disambiguator, not a heuristic that trades one
|
|
135
|
+
misclassification for another."""
|
|
136
|
+
if path.startswith("/"):
|
|
137
|
+
return False
|
|
138
|
+
return "\\" in path or bool(_WINDOWS_DRIVE_RE.match(path))
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _absolute_path_to_file_uri(path: PurePosixPath | PureWindowsPath) -> str:
|
|
142
|
+
"""`path` -- which MUST be absolute by its own oracle's rules -- rendered
|
|
143
|
+
as an absolute `file:` URI, host-independently.
|
|
144
|
+
|
|
145
|
+
This is `pathlib.PurePath.as_uri()`'s own body, verbatim, minus the
|
|
146
|
+
`DeprecationWarning` 3.14 added to it. `PurePath.as_uri()` is deprecated
|
|
147
|
+
and scheduled for REMOVAL in Python 3.19 (tan-cli#1140), and it was this
|
|
148
|
+
module's exporter -- so the warning fired from the product module, not
|
|
149
|
+
only from tests, and the removal would break `tan validate --format
|
|
150
|
+
sarif`'s `artifactLocation.uri` outright. Carrying the body here retires
|
|
151
|
+
the deprecated call while keeping the emitted bytes identical;
|
|
152
|
+
`tests/core/test_uri_reference.py` pins that byte-identity against the
|
|
153
|
+
stdlib itself, per input, on every supported interpreter.
|
|
154
|
+
|
|
155
|
+
**The deprecation's own named replacement, `pathlib.Path.as_uri()`,
|
|
156
|
+
cannot be used here**, and that is measured, not assumed: from 3.14 it
|
|
157
|
+
delegates to `urllib.request.pathname2url(str(self), add_scheme=True)`,
|
|
158
|
+
which dispatches on `os.name`, so the conversion it performs is the
|
|
159
|
+
RUNNING HOST's, not the one the path is spelled in. The damage does not
|
|
160
|
+
disappear on a Windows runner, it MOVES. Measured on 3.14.7 from a POSIX
|
|
161
|
+
host, all nine Windows-spelled shapes in the test corpus come out wrong
|
|
162
|
+
(`C:\\w\\proj\\board.yaml` -> `"file:C%3A%5Cw%5Cproj%5Cboard.yaml"`
|
|
163
|
+
rather than `"file:///C:/w/proj/board.yaml"`) while every POSIX-spelled
|
|
164
|
+
one survives; under a simulated `os.name = "nt"` that inverts, and one of
|
|
165
|
+
the POSIX shapes it then breaks is `/tmp/proj/we\\ird.yaml` ->
|
|
166
|
+
`"file:///tmp/proj/we/ird.yaml"` -- silently naming a DIFFERENT file,
|
|
167
|
+
which is round 1 MAJOR 1, the regression [`_is_windows_spelled`] exists
|
|
168
|
+
to prevent. Below 3.14 there is no divergence on either host, and that is
|
|
169
|
+
what would have made the swap dangerous. The full four-cell account, and
|
|
170
|
+
the assertion that keeps it honest, are on the test file's
|
|
171
|
+
`test_pathlib_path_as_uri_is_not_a_usable_replacement_for_this_exporter`.
|
|
172
|
+
|
|
173
|
+
The deeper reason is structural: `Path` is CONCRETE and host-bound -- a
|
|
174
|
+
`PureWindowsPath` cannot become a `WindowsPath` on a POSIX host at all --
|
|
175
|
+
while [`_is_windows_spelled`] deliberately picks its oracle from the path
|
|
176
|
+
STRING, so BOTH branches are exercised on every runner rather than only
|
|
177
|
+
the one matching that runner's `os.name`. Routing the export through
|
|
178
|
+
`Path` hands that choice back to the host, which is the defect class
|
|
179
|
+
tan-cli#1105 and PR #1125 were bitten by.
|
|
180
|
+
|
|
181
|
+
`os.fsencode` below is the one host-sensitive call, and it is inherited
|
|
182
|
+
verbatim from the stdlib body rather than introduced: it resolves to
|
|
183
|
+
utf-8/`surrogateescape` on POSIX and utf-8/`surrogatepass` on Windows,
|
|
184
|
+
which differ only for a `str` carrying lone surrogates. Keeping it is
|
|
185
|
+
what makes this byte-identical to the call it replaces; it is not a
|
|
186
|
+
branch on `os.name`, and [`_is_windows_spelled`]'s "never the host's
|
|
187
|
+
rules" premise is untouched -- nothing below consults the host to decide
|
|
188
|
+
which SPELLING to emit.
|
|
189
|
+
|
|
190
|
+
The `.is_absolute()` guard is kept, and kept raising, for the reason
|
|
191
|
+
[`path_to_uri_reference`]'s docstring gives: the exporter must enforce
|
|
192
|
+
the very predicate its caller gates on, so gate and exporter cannot
|
|
193
|
+
drift apart. Dropping it would let a relative path through as a
|
|
194
|
+
malformed `file:` URI instead of the `ValueError` the caller's own
|
|
195
|
+
branch is written against."""
|
|
196
|
+
if not path.is_absolute():
|
|
197
|
+
raise ValueError("relative path can't be expressed as a file URI")
|
|
198
|
+
|
|
199
|
+
drive = path.drive
|
|
200
|
+
if len(drive) == 2 and drive[1] == ":":
|
|
201
|
+
# A path on a local drive => `file:///c:/a/b`.
|
|
202
|
+
prefix = "file:///" + drive
|
|
203
|
+
rest = path.as_posix()[2:]
|
|
204
|
+
elif drive:
|
|
205
|
+
# A path on a network drive => `file://host/share/a/b`.
|
|
206
|
+
prefix = "file:"
|
|
207
|
+
rest = path.as_posix()
|
|
208
|
+
else:
|
|
209
|
+
# A POSIX path => `file:///etc/hosts`.
|
|
210
|
+
prefix = "file://"
|
|
211
|
+
rest = str(path)
|
|
212
|
+
return prefix + quote_from_bytes(os.fsencode(rest))
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def path_to_uri_reference(path: str) -> str:
|
|
216
|
+
"""`path` rendered as a valid URI reference (RFC 3986).
|
|
217
|
+
|
|
218
|
+
[`_absolute_path_to_file_uri`] is the exporter for the absolute case,
|
|
219
|
+
and it carries the gate too: `.is_absolute()`, NOT `ntpath.isabs`/
|
|
220
|
+
`posixpath.isabs`. The load-bearing reason is that `.is_absolute()` IS
|
|
221
|
+
the predicate the exporter itself enforces, so the gate and the exporter
|
|
222
|
+
cannot disagree -- on any interpreter, now or later. Gating on a
|
|
223
|
+
DIFFERENT notion of "absolute" would call the exporter on a path it then
|
|
224
|
+
raises `ValueError` on. (That exporter used to be `PurePath.as_uri()`
|
|
225
|
+
called directly here; 3.14 deprecates it for removal in 3.19, so
|
|
226
|
+
tan-cli#1140 carried its body into this module byte-for-byte, guard
|
|
227
|
+
included, rather than let a removal take out `tan validate --format
|
|
228
|
+
sarif`'s `artifactLocation.uri`. See that function's own docstring for
|
|
229
|
+
why the deprecation's named replacement, `Path.as_uri()`, is not usable
|
|
230
|
+
here.)
|
|
231
|
+
|
|
232
|
+
A weaker reason used to stand here, and it has since expired
|
|
233
|
+
(tan-cli#1126). It said the two oracles simply disagree for a
|
|
234
|
+
rooted-but-driveless Windows path -- `ntpath.isabs(r"\\proj\\x")` is
|
|
235
|
+
`True` ("absolute on the current drive") against
|
|
236
|
+
`PureWindowsPath(r"\\proj\\x").is_absolute()`'s `False` (pathlib
|
|
237
|
+
requires BOTH a drive and a root) -- stated as a fixed stdlib fact.
|
|
238
|
+
It is not one. Measured on 3.12.3 / 3.13.15 / 3.14.7: `ntpath.isabs`
|
|
239
|
+
answers that shape `True` on 3.12 and `False` from **3.13** on, where
|
|
240
|
+
CPython made it agree with pathlib; `.is_absolute()` is `False` on all
|
|
241
|
+
three, unchanged. So on every supported interpreter but the `>=3.12`
|
|
242
|
+
floor itself the two now AGREE -- which changes nothing about this
|
|
243
|
+
gate, precisely because the choice never rested on them differing.
|
|
244
|
+
Two tests in `tests/core/test_uri_reference.py` had encoded the 3.12
|
|
245
|
+
answer as universal and failed the full suite on 3.14.7 for it; the
|
|
246
|
+
version-dependent fact now lives in one measured constant there
|
|
247
|
+
(`_NTPATH_ISABS_ACCEPTS_A_DRIVELESS_ROOT`), asserted on both sides of
|
|
248
|
+
the boundary rather than restated in prose here.
|
|
249
|
+
|
|
250
|
+
* An ABSOLUTE `path` becomes an absolute `file:` URI --
|
|
251
|
+
`file:///C:/w/proj/board.yaml` for the Windows-spelled root the issue
|
|
252
|
+
opened on, `file:///w/proj/board.yaml` for a POSIX one -- which is what
|
|
253
|
+
both consumers actually parse as a URI reference. Which oracle renders
|
|
254
|
+
it is decided by [`_is_windows_spelled`], not `os.name`: `PureWindowsPath`
|
|
255
|
+
for a Windows-spelled path, `PurePosixPath` otherwise. (Plain `Path`
|
|
256
|
+
would use the CI HOST's rules and misjudge a Windows-spelled string on
|
|
257
|
+
this repo's Linux CI -- the same trap PR #1089/#1090 already named.)
|
|
258
|
+
[`_absolute_path_to_file_uri`] itself percent-encodes non-ASCII and
|
|
259
|
+
reserved bytes (a space, a literal backslash inside a POSIX filename,
|
|
260
|
+
`#`, non-ASCII -- all measured), so nothing further is needed on this
|
|
261
|
+
branch.
|
|
262
|
+
* A RELATIVE, POSIX-spelled `path` (`resolve_board_path`'s own default,
|
|
263
|
+
`"./board.yaml"`) is returned percent-encoded but otherwise UNCHANGED
|
|
264
|
+
-- see this module's docstring's "relative case" section for why it
|
|
265
|
+
stays relative instead of being absolutised. `urllib.parse.quote`
|
|
266
|
+
(`safe="/"`, so the separator itself is untouched) closes the
|
|
267
|
+
remaining gap round 1 measured: a raw space or `#` in a relative
|
|
268
|
+
`path` is not a legal URI-reference character/is a fragment
|
|
269
|
+
delimiter, and the separator-less golden (`"./board.yaml"`) contains
|
|
270
|
+
neither, so quoting it is a no-op and the pin is untouched.
|
|
271
|
+
* A RELATIVE, WINDOWS-spelled `path` (`resolve_board_path("sub\\p",
|
|
272
|
+
None)`'s shape, `"sub\\p\\board.yaml"`) has its backslashes swapped for
|
|
273
|
+
forward slashes, then the same percent-encoding applied. A bare
|
|
274
|
+
backslash is not a legal character in a URI reference at all (RFC
|
|
275
|
+
3986's `pchar` excludes it unencoded) -- leaving it in would still be
|
|
276
|
+
invalid, just differently invalid, and Windows itself accepts `/` as
|
|
277
|
+
an equally valid path separator, so the swap is meaning-preserving.
|
|
278
|
+
|
|
279
|
+
Known, deliberately undefended edge cases (round 1 nits, low
|
|
280
|
+
reachability from this repo's own resolver): a `\\\\?\\`-prefixed
|
|
281
|
+
Windows extended-length path renders its `\\\\?\\` host segment through
|
|
282
|
+
[`_absolute_path_to_file_uri`] unexamined (`file://%3F/C%3A/...`, the
|
|
283
|
+
drive demoted into the path) rather than being rejected or normalised;
|
|
284
|
+
and drive-letter casing is preserved verbatim from the input rather than
|
|
285
|
+
canonicalised, so `"c:/w/x"` and `"C:\\w\\x"` for the same file produce
|
|
286
|
+
differently-cased URIs. Neither is exercised by anything
|
|
287
|
+
`resolve_board_path` itself produces.
|
|
288
|
+
"""
|
|
289
|
+
if _is_windows_spelled(path):
|
|
290
|
+
win = PureWindowsPath(path)
|
|
291
|
+
if win.is_absolute():
|
|
292
|
+
return _absolute_path_to_file_uri(win)
|
|
293
|
+
return quote(path.replace("\\", "/"), safe="/")
|
|
294
|
+
posix = PurePosixPath(path)
|
|
295
|
+
if posix.is_absolute():
|
|
296
|
+
return _absolute_path_to_file_uri(posix)
|
|
297
|
+
return quote(path, safe="/")
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def is_absolute_path_reference(path: str) -> bool:
|
|
301
|
+
"""Whether [`path_to_uri_reference`] renders `path` as an ABSOLUTE
|
|
302
|
+
`file:` URI -- the gate `_sarif_document` (tan-cli#1117) uses to decide
|
|
303
|
+
whether a location may carry SARIF's `uriBaseId` at all: 2.1.0 SS3.4.4
|
|
304
|
+
-- "If the uri property is an absolute URI, the uriBaseId property SHALL
|
|
305
|
+
be absent."
|
|
306
|
+
|
|
307
|
+
Delegates to [`path_to_uri_reference`] itself rather than RE-DERIVING
|
|
308
|
+
the same `_is_windows_spelled` + `.is_absolute()` branch a second time:
|
|
309
|
+
a `file:` scheme in that function's OUTPUT is produced by exactly its
|
|
310
|
+
two [`_absolute_path_to_file_uri`] calls, both already gated on the
|
|
311
|
+
identical `is_absolute()` check a second copy here would only
|
|
312
|
+
duplicate -- drifting the two apart the moment [`path_to_uri_reference`]
|
|
313
|
+
grows a branch this one does not learn about is exactly the
|
|
314
|
+
per-command-copy class of defect `tan.core.board_context`'s own module
|
|
315
|
+
docstring names for a resolver retyped instead of shared.
|
|
316
|
+
|
|
317
|
+
A first version of this docstring worried the sniff was unsound for a
|
|
318
|
+
driveless Windows path (`"C:board.yaml"`) on the theory that its
|
|
319
|
+
rendered form is "quoted unchanged" and therefore a syntactically valid
|
|
320
|
+
URI with scheme `C`. Measured, that is false: `quote(..., safe="/")`
|
|
321
|
+
does not treat `:` as safe, so `path_to_uri_reference("C:board.yaml")`
|
|
322
|
+
is actually `"C%3Aboard.yaml"` -- the colon is percent-encoded, and
|
|
323
|
+
`.startswith("file:")` on it is `False`, correctly. The SAME encoding
|
|
324
|
+
rule is what makes the sniff safe in general: a `file:` PREFIX can only
|
|
325
|
+
ever appear in the output through the two absolute-only
|
|
326
|
+
[`_absolute_path_to_file_uri`] branches -- any RELATIVE reference
|
|
327
|
+
containing a literal `"file:"` substring has its colon percent-encoded
|
|
328
|
+
before this check ever sees it
|
|
329
|
+
(`path_to_uri_reference("file:sub/board.yaml") == "file%3Asub/board
|
|
330
|
+
.yaml"`, measured), so it can never masquerade as one."""
|
|
331
|
+
return path_to_uri_reference(path).startswith("file:")
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def cwd_base_uri() -> str:
|
|
335
|
+
"""The SARIF `originalUriBaseIds` base for a RELATIVE `artifactLocation
|
|
336
|
+
.uri` (tan-cli#1117): the process's own working directory, as an
|
|
337
|
+
absolute `file:` URI with a TRAILING SLASH. **Can raise** -- see the
|
|
338
|
+
last section below; [`cwd_base_uri_or_none`] is the guarded call site
|
|
339
|
+
should actually use.
|
|
340
|
+
|
|
341
|
+
**Anchored on `Path.cwd()`, never on `--project`/`root`.**
|
|
342
|
+
`resolve_board_path` already prepends `root` onto the relative reference
|
|
343
|
+
itself (`--project sub` renders `"sub/board.yaml"`, not `"board.yaml"`),
|
|
344
|
+
so the reference is CWD-relative by construction. A base ALSO anchored on
|
|
345
|
+
`root` would apply that prefix a SECOND time -- `urljoin("file://.../sub/
|
|
346
|
+
", "sub/board.yaml")` names `.../sub/sub/board.yaml`, which does not
|
|
347
|
+
exist -- reproducing the exact tan-cli#1097-round-2 defect this issue was
|
|
348
|
+
filed to fix (measured there: `root="."`'s base resolved `./board.yaml`
|
|
349
|
+
onto the WRONG file because the base and the reference were anchored to
|
|
350
|
+
two different roots). `tests/core/test_uri_reference.py`'s mutation for
|
|
351
|
+
this line swaps `Path.cwd()` for a `root`-derived path and reds on the
|
|
352
|
+
`--project sub` case for exactly that reason.
|
|
353
|
+
|
|
354
|
+
**The trailing slash is not decorative, and not unconditional.** RFC 3986
|
|
355
|
+
SS5.3's reference-resolution algorithm drops the base's own LAST PATH
|
|
356
|
+
SEGMENT before appending a relative reference: `urljoin("file:///a/b",
|
|
357
|
+
"c")` names `"file:///a/c"`, not `"file:///a/b/c"` -- a slash-less base
|
|
358
|
+
names a RESOURCE, indistinguishable from a file, one level above the
|
|
359
|
+
directory it reads as. Dropping the slash entirely reproduces the same
|
|
360
|
+
wrong-file mismatch tan-cli#1117 measured (`EXISTS? False` against the
|
|
361
|
+
real, differently-pathed file). But `Path.as_uri()` DOES already append
|
|
362
|
+
one at a filesystem root -- measured, `Path("/").as_uri() ==
|
|
363
|
+
"file:///"` -- so appending UNCONDITIONALLY doubles it there
|
|
364
|
+
(`"file:////"`, and `PureWindowsPath("C:/").as_uri()` -> `"file:///C://"`
|
|
365
|
+
the same way). SARIF 2.1.0 SS3.14.14 requires the value "SHALL end with a
|
|
366
|
+
single forward slash", NOTE 2 explaining that resolution works "by
|
|
367
|
+
simple concatenation" -- a doubled slash breaks exactly that
|
|
368
|
+
concatenation (`"file:////" + "./board.yaml"` is not the intended path,
|
|
369
|
+
even though `urljoin` itself tolerates it and recovers). Appending only
|
|
370
|
+
when not already slash-terminated keeps both properties -- always ends
|
|
371
|
+
in exactly one slash, never two.
|
|
372
|
+
|
|
373
|
+
**`Path.cwd()` CAN raise, even though it touches no caller-supplied
|
|
374
|
+
data.** `Path.cwd()` -> `os.getcwd()` raises `FileNotFoundError` (an
|
|
375
|
+
`OSError` subclass) when the process's OWN working directory has been
|
|
376
|
+
removed out from under it -- measured directly. This function does not
|
|
377
|
+
guard it: [`cwd_base_uri_or_none`] is the call site `validate_cmd.py`
|
|
378
|
+
actually uses, and it exists SPECIFICALLY because tan-cli#1117 review
|
|
379
|
+
round 2 measured that an unguarded call here reached `_sarif_document`
|
|
380
|
+
from INSIDE `validate_cmd.py`'s own `except Exception as err:` handler
|
|
381
|
+
and double-faulted the same way the reverted round-1 attempt's
|
|
382
|
+
`Path.resolve(strict=False)` symlink-loop crash did -- a `#1116`
|
|
383
|
+
never-raises instance reached through a filesystem call that happens not
|
|
384
|
+
to be caller-supplied, rather than through caller data itself. (For the
|
|
385
|
+
record, since it is the OTHER crash surface tan-cli#1117 was filed over:
|
|
386
|
+
`.resolve()`'s own symlink-loop behaviour is not uniform across pathlib
|
|
387
|
+
versions either -- measured directly, `python-build-standalone`
|
|
388
|
+
3.12.3/3.13.15/3.14.7: on 3.12, `Path("loop").resolve(strict=False)`
|
|
389
|
+
raises `RuntimeError` regardless of `strict=False`'s own promise not to;
|
|
390
|
+
on 3.13/3.14 that promise IS kept for `strict=False` (returns the path
|
|
391
|
+
unresolved, no raise), but `strict=True` still raises `OSError [Errno
|
|
392
|
+
40]` on both. This function calls neither `.resolve()` nor anything on a
|
|
393
|
+
caller-supplied path at all, so that particular crash class cannot recur
|
|
394
|
+
here regardless.)"""
|
|
395
|
+
base = Path.cwd().as_uri()
|
|
396
|
+
return base if base.endswith("/") else base + "/"
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def cwd_base_uri_or_none() -> str | None:
|
|
400
|
+
"""[`cwd_base_uri`], guarded against the one way it can raise: the
|
|
401
|
+
process's own working directory has been removed. `validate_cmd.py`'s
|
|
402
|
+
`validate()` calls this ONCE, before any guard in its own body can fire,
|
|
403
|
+
and threads the result through `_emit`/`_sarif_document` as a plain
|
|
404
|
+
value -- so nothing downstream of this call touches the filesystem for
|
|
405
|
+
the SARIF base again, and nothing there can raise from a missing CWD no
|
|
406
|
+
matter which branch of `validate()` eventually reaches the SARIF
|
|
407
|
+
exporter (tan-cli#1117 review round 2's blocker).
|
|
408
|
+
|
|
409
|
+
`None` means exactly what `dev` emitted before tan-cli#1117: a relative
|
|
410
|
+
`uri` with no declared base -- the safe fallback, not a degraded one;
|
|
411
|
+
see `_sarif_document`'s own docstring for how it is threaded through."""
|
|
412
|
+
try:
|
|
413
|
+
return cwd_base_uri()
|
|
414
|
+
except OSError:
|
|
415
|
+
return None
|
tan/core/user_defines.py
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""User `-D NAME=VALUE` on a PLANNED build (tan-cli#1382).
|
|
3
|
+
|
|
4
|
+
A planned (`board.yaml`) build's Zephyr slice carries argv the planner emitted
|
|
5
|
+
(`west build -b <board> <app> -- -DPython3_EXECUTABLE=... -DEXTRA_CONF_FILE=...`).
|
|
6
|
+
alp-sdk examples document extra CMake definitions on top of that
|
|
7
|
+
(`-DSHIELD=...`, `-DCONFIG_...=...`), so `tan build -D` splices the user's
|
|
8
|
+
definitions into the plan BEFORE token substitution and dispatch:
|
|
9
|
+
|
|
10
|
+
* user `-D` goes AFTER the plan-emitted args, so CMake's last-wins lets it
|
|
11
|
+
override anything the plan set -- except the keys tan owns for correctness;
|
|
12
|
+
* `EXTRA_CONF_FILE` / `EXTRA_DTC_OVERLAY_FILE` (and a sysbuild `<image>_`
|
|
13
|
+
prefixed form) are lists tan already feeds (the per-core `alp.conf`), so the
|
|
14
|
+
user's value is APPENDED `;`-joined to the plan's, never a replacement;
|
|
15
|
+
* `BOARD` / `Python3_EXECUTABLE` are refused -- tan derives both.
|
|
16
|
+
|
|
17
|
+
Pure data-shaping: nothing here resolves or spawns anything.
|
|
18
|
+
"""
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import json
|
|
22
|
+
import re
|
|
23
|
+
|
|
24
|
+
#: Keys tan owns outright; a user value would desynchronise the build.
|
|
25
|
+
RESERVED_KEYS = frozenset({"BOARD", "Python3_EXECUTABLE"})
|
|
26
|
+
#: Keys whose user value joins tan's list (`;`, a CMake list).
|
|
27
|
+
_APPEND_KEY = re.compile(
|
|
28
|
+
r"(?:[A-Za-z0-9_]+_)?(?:EXTRA_(?:CONF|DTC_OVERLAY)_FILE|SB_CONF_FILE|SB_EXTRA_CONF_FILE)"
|
|
29
|
+
)
|
|
30
|
+
_NAME = re.compile(r"-D([A-Za-z_][A-Za-z0-9_]*)(?::[A-Za-z]+)?(=.*)?", re.DOTALL)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class UserDefineError(Exception):
|
|
34
|
+
def __init__(self, code: str, message: str) -> None:
|
|
35
|
+
super().__init__(message)
|
|
36
|
+
self.code = code
|
|
37
|
+
self.message = message
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _split(define: str) -> tuple[str, str | None]:
|
|
41
|
+
m = _NAME.fullmatch(define)
|
|
42
|
+
assert m is not None # already validated by `normalise_defines`
|
|
43
|
+
return m.group(1), (m.group(2)[1:] if m.group(2) is not None else None)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def user_defines_problem(defines: list[str]) -> tuple[str, str] | None:
|
|
47
|
+
"""`(code, message)` for a normalised `-D` list tan must refuse, else None."""
|
|
48
|
+
for d in defines:
|
|
49
|
+
if any(ord(c) < 0x20 or ord(c) == 0x7F for c in d):
|
|
50
|
+
return (
|
|
51
|
+
"build.invalid-argument",
|
|
52
|
+
f"`-D {d!r}` contains a control character (newline, tab, ...); "
|
|
53
|
+
"a CMake definition is one line.",
|
|
54
|
+
)
|
|
55
|
+
name, value = _split(d)
|
|
56
|
+
if name in RESERVED_KEYS:
|
|
57
|
+
return (
|
|
58
|
+
"build.define-reserved",
|
|
59
|
+
f"`-D {name}` cannot be overridden: tan derives `{name}` itself "
|
|
60
|
+
"(the board comes from the plan / `--board`, Python from the resolved host Python).",
|
|
61
|
+
)
|
|
62
|
+
if _APPEND_KEY.fullmatch(name) and not value:
|
|
63
|
+
return (
|
|
64
|
+
"build.invalid-argument",
|
|
65
|
+
f"`-D {name}` needs a value: it is appended to tan's own `{name}` list.",
|
|
66
|
+
)
|
|
67
|
+
return None
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _apply_to_args(args: list[str], defines: list[str]) -> list[str]:
|
|
71
|
+
out = list(args)
|
|
72
|
+
if "--" not in out:
|
|
73
|
+
out.append("--")
|
|
74
|
+
for d in defines:
|
|
75
|
+
name, value = _split(d)
|
|
76
|
+
if _APPEND_KEY.fullmatch(name):
|
|
77
|
+
idx = next(
|
|
78
|
+
(
|
|
79
|
+
i
|
|
80
|
+
for i in range(len(out) - 1, out.index("--"), -1)
|
|
81
|
+
if (m := _NAME.fullmatch(out[i])) is not None
|
|
82
|
+
and m.group(1) == name
|
|
83
|
+
and m.group(2) is not None
|
|
84
|
+
),
|
|
85
|
+
None,
|
|
86
|
+
)
|
|
87
|
+
if idx is not None:
|
|
88
|
+
out[idx] = f"{out[idx]};{value}"
|
|
89
|
+
continue
|
|
90
|
+
out.append(d)
|
|
91
|
+
return out
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def apply_user_defines(
|
|
95
|
+
plan_text: str, defines: list[str], cores: list[str] | None = None
|
|
96
|
+
) -> tuple[str, list[str]]:
|
|
97
|
+
"""The plan text with `defines` spliced into the targeted Zephyr slices'
|
|
98
|
+
argv, plus the coreIds changed. Targets: every Zephyr slice that has a
|
|
99
|
+
command, or only `cores` when given. Raises [`UserDefineError`] when `cores`
|
|
100
|
+
names a slice that is not such a target, or nothing is targeted."""
|
|
101
|
+
plan = json.loads(plan_text)
|
|
102
|
+
targets = [
|
|
103
|
+
s
|
|
104
|
+
for s in plan.get("slices", [])
|
|
105
|
+
if s.get("backend") == "zephyr" and isinstance(s.get("command"), dict)
|
|
106
|
+
]
|
|
107
|
+
if cores:
|
|
108
|
+
known = {s["coreId"] for s in targets}
|
|
109
|
+
missing = [c for c in cores if c not in known]
|
|
110
|
+
if missing:
|
|
111
|
+
raise UserDefineError(
|
|
112
|
+
"build.invalid-argument",
|
|
113
|
+
f"`--core {', '.join(missing)}` is not a Zephyr slice of this plan "
|
|
114
|
+
f"(Zephyr slices: {', '.join(sorted(known)) or 'none'}).",
|
|
115
|
+
)
|
|
116
|
+
targets = [s for s in targets if s["coreId"] in cores]
|
|
117
|
+
if not targets:
|
|
118
|
+
raise UserDefineError(
|
|
119
|
+
"build.define-no-target",
|
|
120
|
+
"`-D` has no Zephyr slice to apply to: this plan has none with a build command.",
|
|
121
|
+
)
|
|
122
|
+
for s in targets:
|
|
123
|
+
s["command"]["args"] = _apply_to_args(s["command"]["args"], defines)
|
|
124
|
+
return json.dumps(plan, indent=2), [s["coreId"] for s in targets]
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def define_pairs(defines: list[str]) -> list[str]:
|
|
128
|
+
"""`NAME=VALUE` (or bare `NAME`) lines for normalised `-D` args -- the unit
|
|
129
|
+
the per-slice stamp records."""
|
|
130
|
+
return sorted(d[2:] for d in defines)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def changed_defines(previous: list[str], current: list[str]) -> list[str]:
|
|
134
|
+
"""Names whose `-D` was added, removed or given a different value between
|
|
135
|
+
two stamped sets (sorted, de-duplicated)."""
|
|
136
|
+
diff = set(previous) ^ set(current)
|
|
137
|
+
return sorted({re.split(r"[:=]", d, maxsplit=1)[0] for d in diff})
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def insert_after_dashdash(args: list[str], extra: list[str]) -> list[str]:
|
|
141
|
+
"""`extra` placed right after `--`, i.e. BEFORE every plan/user `-D`.
|
|
142
|
+
|
|
143
|
+
Order is load-bearing: cmake applies `-U`/`-D` in argv order, so a `-U<key>`
|
|
144
|
+
after a `-D<key>=...` silently unsets it. Before them, a `-D` of the same
|
|
145
|
+
key wins. (Adds the `--` when the argv has none.)"""
|
|
146
|
+
if not extra:
|
|
147
|
+
return list(args)
|
|
148
|
+
out = list(args)
|
|
149
|
+
if "--" not in out:
|
|
150
|
+
out.append("--")
|
|
151
|
+
i = out.index("--") + 1
|
|
152
|
+
return out[:i] + list(extra) + out[i:]
|