xt-cli 0.2.1__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.
errors.py ADDED
@@ -0,0 +1,29 @@
1
+ """xt-cli 异常定义。"""
2
+
3
+
4
+ class XtCliError(Exception):
5
+ """xt-cli 基础异常。"""
6
+
7
+
8
+ class CliUsageError(XtCliError):
9
+ """命令参数使用错误。"""
10
+
11
+
12
+ class ConfigError(XtCliError):
13
+ """配置相关错误。"""
14
+
15
+
16
+ class ProjectError(XtCliError):
17
+ """项目相关错误。"""
18
+
19
+
20
+ class TargetResolveError(XtCliError):
21
+ """目标解析错误。"""
22
+
23
+
24
+ class XmakeInvokeError(XtCliError):
25
+ """xmake 调用错误。"""
26
+
27
+
28
+ class HookError(XtCliError):
29
+ """Hook 执行错误。"""
hooks.py ADDED
@@ -0,0 +1,317 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import importlib.util
5
+ import types
6
+ from collections.abc import Callable
7
+ from dataclasses import dataclass
8
+ from pathlib import Path
9
+
10
+ from models import BuildContext
11
+ from output import print_step
12
+
13
+ _BUILTIN_TASKS = {"build", "clean", "fullclean"}
14
+
15
+
16
+ @dataclass(slots=True, frozen=True)
17
+ class HookResult:
18
+ """Hook 调用结果。"""
19
+
20
+ handled: bool
21
+ returncode: int = 0
22
+
23
+
24
+ def _load_hook_module(hook_path: Path) -> types.ModuleType | None:
25
+ """动态加载 hook.py 模块,不生成字节码缓存。"""
26
+ if not hook_path.is_file():
27
+ return None
28
+ try:
29
+ spec = importlib.util.spec_from_file_location(
30
+ "xt_hook", hook_path,
31
+ submodule_search_locations=[],
32
+ )
33
+ if spec is None or spec.origin is None:
34
+ return None
35
+ # 用 compile + exec 替代 exec_module,避免生成 __pycache__
36
+ source = hook_path.read_text(encoding="utf-8")
37
+ code = compile(source, str(hook_path), "exec")
38
+ module = types.ModuleType("xt_hook")
39
+ module.__file__ = str(hook_path)
40
+ exec(code, module.__dict__) # noqa: S102
41
+ return module
42
+ except Exception:
43
+ return None
44
+
45
+
46
+ def _try_dispatch(
47
+ hook_path: Path,
48
+ task: str,
49
+ context: BuildContext,
50
+ global_args: argparse.Namespace,
51
+ shared_args: argparse.Namespace,
52
+ self_args: argparse.Namespace,
53
+ unknown_args: list[str],
54
+ source: str = "",
55
+ ) -> HookResult:
56
+ """尝试从指定 hook 文件分发 task,有特定 handler 时预打印。"""
57
+ module = _load_hook_module(hook_path)
58
+ if module is None:
59
+ return HookResult(handled=False)
60
+ if not callable(getattr(module, f"handle_{task}", None)) and not callable(getattr(module, "handle", None)):
61
+ return HookResult(handled=False)
62
+ if callable(getattr(module, f"handle_{task}", None)):
63
+ print_step(f"{task}, via {source} hook")
64
+
65
+ # 临时注入 dispatch 参数,供 context.run_task() 自动透传
66
+ prev = getattr(context, "_dispatch_args", None)
67
+ object.__setattr__(context, "_dispatch_args", (global_args, shared_args, self_args, unknown_args))
68
+ try:
69
+ return _call_hook(module, task, context, global_args, shared_args, self_args, unknown_args)
70
+ finally:
71
+ object.__setattr__(context, "_dispatch_args", prev)
72
+
73
+
74
+ def _call_hook(
75
+ module: types.ModuleType,
76
+ task: str,
77
+ context: BuildContext,
78
+ global_args: argparse.Namespace,
79
+ shared_args: argparse.Namespace,
80
+ self_args: argparse.Namespace,
81
+ unknown_args: list[str],
82
+ ) -> HookResult:
83
+ """调用 hook 模块中的处理函数。
84
+
85
+ 返回 None 表示不处理此 task,视为 unhandled。
86
+ """
87
+ specific_fn = getattr(module, f"handle_{task}", None)
88
+ if callable(specific_fn):
89
+ returncode = specific_fn(context, global_args, shared_args, self_args, unknown_args)
90
+ if returncode is None:
91
+ return HookResult(handled=False)
92
+ return HookResult(handled=True, returncode=returncode)
93
+
94
+ generic_fn = getattr(module, "handle", None)
95
+ if callable(generic_fn):
96
+ returncode = generic_fn(context, task, unknown_args)
97
+ if returncode is None:
98
+ return HookResult(handled=False)
99
+ return HookResult(handled=True, returncode=returncode)
100
+
101
+ return HookResult(handled=False)
102
+
103
+
104
+ def run_platform_hook(
105
+ context: BuildContext,
106
+ task: str,
107
+ unknown_args: list[str],
108
+ global_args: argparse.Namespace,
109
+ shared_args: argparse.Namespace,
110
+ self_args: argparse.Namespace,
111
+ ) -> HookResult:
112
+ """运行平台级 hook。"""
113
+ hook_path = context.target_path.parent / "xt_hook.py"
114
+ module = _load_hook_module(hook_path)
115
+ if module is None:
116
+ return HookResult(handled=False)
117
+ return _call_hook(module, task, context, global_args, shared_args, self_args, unknown_args)
118
+
119
+
120
+ def run_project_hook(
121
+ context: BuildContext,
122
+ task: str,
123
+ unknown_args: list[str],
124
+ global_args: argparse.Namespace,
125
+ shared_args: argparse.Namespace,
126
+ self_args: argparse.Namespace,
127
+ ) -> HookResult:
128
+ """运行工程级 hook。"""
129
+ hook_path = context.project_dir / "xt_hook.py"
130
+ module = _load_hook_module(hook_path)
131
+ if module is None:
132
+ return HookResult(handled=False)
133
+ return _call_hook(module, task, context, global_args, shared_args, self_args, unknown_args)
134
+
135
+
136
+ def _run_builtin_task(
137
+ context: BuildContext,
138
+ task: str,
139
+ unknown_args: list[str],
140
+ global_args: argparse.Namespace | None = None,
141
+ shared_args: argparse.Namespace | None = None,
142
+ self_args: argparse.Namespace | None = None,
143
+ ) -> int:
144
+ """直接执行内置 task(不经过 hook),用于递归保护时的 fallback。"""
145
+ if global_args is None:
146
+ global_args = argparse.Namespace()
147
+ if shared_args is None:
148
+ shared_args = argparse.Namespace()
149
+ if self_args is None:
150
+ self_args = argparse.Namespace()
151
+
152
+ if task == "build":
153
+ from xmake import build_command, run_xmake
154
+
155
+ before_ret = run_lifecycle_hook(context, "before_build", unknown_args, global_args, shared_args, self_args)
156
+ if before_ret != 0:
157
+ return before_ret
158
+
159
+ print_step("build")
160
+ result = run_xmake(context, build_command(context))
161
+ build_ret = result.returncode
162
+
163
+ run_lifecycle_hook(context, "after_build", unknown_args, global_args, shared_args, self_args)
164
+ return build_ret
165
+ elif task == "clean":
166
+ from xmake import build_clean_command, run_xmake
167
+
168
+ before_ret = run_lifecycle_hook(context, "before_clean", unknown_args, global_args, self_args)
169
+ if before_ret != 0:
170
+ return before_ret
171
+
172
+ print_step("clean")
173
+ result = run_xmake(context, build_clean_command(context))
174
+ clean_ret = result.returncode
175
+
176
+ run_lifecycle_hook(context, "after_clean", unknown_args, global_args, self_args)
177
+ return clean_ret
178
+ elif task == "fullclean":
179
+ import shutil
180
+
181
+ before_ret = run_lifecycle_hook(context, "before_fullclean", unknown_args, global_args, self_args)
182
+ if before_ret != 0:
183
+ return before_ret
184
+
185
+ print_step("fullclean")
186
+ shutil.rmtree(context.project_dir / ".xmake", ignore_errors=True)
187
+ shutil.rmtree(context.project_dir / ".build", ignore_errors=True)
188
+
189
+ run_lifecycle_hook(context, "after_fullclean", unknown_args, global_args, self_args)
190
+ return 0
191
+ return 1
192
+
193
+
194
+ def dispatch_hook(
195
+ context: BuildContext,
196
+ task: str,
197
+ unknown_args: list[str],
198
+ *,
199
+ dispatching_tasks: set[str] | None = None,
200
+ global_args: argparse.Namespace | None = None,
201
+ shared_args: argparse.Namespace | None = None,
202
+ self_args: argparse.Namespace | None = None,
203
+ ) -> HookResult:
204
+ """按优先级分发 hook:工程优先,未被处理再平台。
205
+
206
+ dispatching_tasks 用于递归保护:如果 task 在集合中,
207
+ 跳过 hook 直接执行内置行为。
208
+ """
209
+ if dispatching_tasks is None:
210
+ dispatching_tasks = set()
211
+ if global_args is None:
212
+ global_args = argparse.Namespace()
213
+ if shared_args is None:
214
+ shared_args = argparse.Namespace()
215
+ if self_args is None:
216
+ self_args = argparse.Namespace()
217
+
218
+ # 递归保护:同 task 直接走内置行为
219
+ if task in dispatching_tasks and task in _BUILTIN_TASKS:
220
+ returncode = _run_builtin_task(context, task, unknown_args, global_args, shared_args, self_args)
221
+ return HookResult(handled=True, returncode=returncode)
222
+
223
+ # 工程 hook → 平台 hook
224
+ project_path = context.project_dir / "xt_hook.py"
225
+ result = _try_dispatch(project_path, task, context, global_args, shared_args, self_args, unknown_args, source="project")
226
+ if result.handled:
227
+ return result
228
+
229
+ platform_path = context.target_path.parent / "xt_hook.py"
230
+ if platform_path != project_path:
231
+ result = _try_dispatch(platform_path, task, context, global_args, shared_args, self_args, unknown_args, source="platform")
232
+ if result.handled:
233
+ return result
234
+
235
+ return HookResult(handled=False)
236
+
237
+
238
+ def run_lifecycle_hook(
239
+ context: BuildContext,
240
+ lifecycle: str,
241
+ unknown_args: list[str] | None = None,
242
+ global_args: argparse.Namespace | None = None,
243
+ shared_args: argparse.Namespace | None = None,
244
+ self_args: argparse.Namespace | None = None,
245
+ ) -> int:
246
+ """运行生命周期 hook(before_build 等),未定义则静默跳过。"""
247
+ result = dispatch_hook(
248
+ context, lifecycle, unknown_args or [],
249
+ global_args=global_args or argparse.Namespace(),
250
+ shared_args=shared_args or argparse.Namespace(),
251
+ self_args=self_args or argparse.Namespace(),
252
+ )
253
+ return result.returncode if result.handled else 0
254
+
255
+
256
+ _LIFECYCLE_PREFIXES = ("before_", "after_")
257
+
258
+
259
+ def discover_hook_tasks(context: BuildContext) -> tuple[list[tuple[str, Callable | None, str, str | None]], dict[str, Callable]]:
260
+ """扫描工程和平台 hook,返回 (task 列表, scope_parsers 字典)。
261
+
262
+ 排除生命周期 hook(before_* / after_*)。
263
+ """
264
+ from collections.abc import Callable as CallableType
265
+
266
+ results: list[tuple[str, CallableType | None, str, str | None]] = []
267
+ scope_parsers: dict[str, CallableType] = {}
268
+ seen: set[str] = set()
269
+
270
+ project_hook_path = context.project_dir / "xt_hook.py"
271
+ module = _load_hook_module(project_hook_path)
272
+ if module is not None:
273
+ _collect_hook_task_names(module, results, seen, "project")
274
+ _collect_scope_parsers(module, "project", scope_parsers)
275
+
276
+ platform_hook_path = context.target_path.parent / "xt_hook.py"
277
+ if platform_hook_path != project_hook_path:
278
+ module = _load_hook_module(platform_hook_path)
279
+ if module is not None:
280
+ source = f"platform/{context.platform_name}"
281
+ _collect_hook_task_names(module, results, seen, source)
282
+ _collect_scope_parsers(module, source, scope_parsers)
283
+
284
+ return results, scope_parsers
285
+
286
+
287
+ def _collect_scope_parsers(module: types.ModuleType, source: str, scope_parsers: dict[str, Callable]) -> None:
288
+ """从 hook 模块中收集 scope 级 parser 函数。"""
289
+ for fn_name in ("add_shared_args_builtin", "add_shared_args_platform", "add_shared_args_project"):
290
+ fn = getattr(module, fn_name, None)
291
+ if callable(fn) and source not in scope_parsers:
292
+ scope_parsers[source] = fn
293
+
294
+
295
+ def _collect_hook_task_names(module: types.ModuleType, results: list[tuple[str, Callable | None, str, str | None]], seen: set[str], source: str) -> None:
296
+ """从 hook 模块中收集 task 名称、add_parser 及 --help 描述。"""
297
+ for name in dir(module):
298
+ if name.startswith("handle_") and callable(getattr(module, name)):
299
+ task_name = name[len("handle_"):]
300
+ if not any(task_name.startswith(p) for p in _LIFECYCLE_PREFIXES):
301
+ if task_name not in seen:
302
+ fn = getattr(module, name)
303
+ help_desc = _extract_help_from_doc(fn)
304
+ add_parser = getattr(module, f"add_args_{task_name}", None)
305
+ results.append((task_name, add_parser if callable(add_parser) else None, source, help_desc))
306
+ seen.add(task_name)
307
+
308
+
309
+ def _extract_help_from_doc(fn) -> str | None:
310
+ """从函数 docstring 中提取 --help: 前缀行作为简短描述。"""
311
+ if not fn.__doc__:
312
+ return None
313
+ for line in fn.__doc__.strip().splitlines():
314
+ stripped = line.strip()
315
+ if stripped.startswith("--help:"):
316
+ return stripped[len("--help:"):].strip()
317
+ return None
models.py ADDED
@@ -0,0 +1,107 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+ try:
8
+ from enum import StrEnum
9
+ except ImportError: # Python 3.10
10
+ from enum import Enum
11
+
12
+ class StrEnum(str, Enum):
13
+ pass
14
+
15
+
16
+ class ConfigSource(StrEnum):
17
+ """配置来源。"""
18
+
19
+ LOCAL = "local"
20
+ GLOBAL = "global"
21
+ DEFAULT = "default"
22
+ CLI = "cli"
23
+ DERIVED = "derived"
24
+
25
+
26
+ @dataclass(slots=True, frozen=True)
27
+ class ResolvedValue:
28
+ """解析后的配置值及其来源。"""
29
+
30
+ value: Any
31
+ source: ConfigSource
32
+ source_path: str | None = None
33
+
34
+
35
+ @dataclass(slots=True, frozen=True)
36
+ class BuildContext:
37
+ """构建上下文。"""
38
+
39
+ project_dir: Path
40
+ sdk_dir: Path
41
+ target: str
42
+ target_path: Path
43
+ platform_name: str
44
+ board: str = ""
45
+ toolchain_path: Path | None = None
46
+ debug_enabled: bool = False
47
+ resolved_values: dict[str, ResolvedValue] | None = None
48
+ _dispatch_args: Any = field(default=None, init=False, repr=False)
49
+
50
+ def run_task(
51
+ self,
52
+ task: str,
53
+ args: list[str] | None = None,
54
+ *,
55
+ global_args: Any = None,
56
+ shared_args: Any = None,
57
+ self_args: Any = None,
58
+ unknown_args: list[str] | None = None,
59
+ ) -> int:
60
+ """执行指定 task,支持组合命令。
61
+
62
+ 自动从当前 handler 的 dispatch 上下文读取 global_args/shared_args 等,
63
+ 调用方无需显式传入。同 task 递归时跳过 hook,直接执行内置行为。
64
+ """
65
+ from hooks import _BUILTIN_TASKS, _run_builtin_task, dispatch_hook
66
+
67
+ dispatch = getattr(self, "_dispatch_args", None)
68
+ if global_args is None and dispatch is not None:
69
+ global_args = dispatch[0]
70
+ if shared_args is None and dispatch is not None:
71
+ shared_args = dispatch[1]
72
+ if self_args is None and dispatch is not None:
73
+ self_args = dispatch[2]
74
+ if unknown_args is None and dispatch is not None:
75
+ unknown_args = dispatch[3]
76
+
77
+ actual_args = args or []
78
+ actual_unknown = unknown_args if unknown_args is not None else actual_args
79
+
80
+ if task in _BUILTIN_TASKS:
81
+ result = dispatch_hook(
82
+ self, task, actual_unknown,
83
+ dispatching_tasks={task},
84
+ global_args=global_args,
85
+ shared_args=shared_args,
86
+ self_args=self_args,
87
+ )
88
+ if result.handled:
89
+ return result.returncode
90
+ return _run_builtin_task(
91
+ self, task, actual_unknown,
92
+ global_args=global_args,
93
+ shared_args=shared_args,
94
+ self_args=self_args,
95
+ )
96
+
97
+ result = dispatch_hook(
98
+ self, task, actual_unknown,
99
+ global_args=global_args,
100
+ shared_args=shared_args,
101
+ self_args=self_args,
102
+ )
103
+ if not result.handled:
104
+ import sys
105
+ print(f"Unknown task: {task}", file=sys.stderr)
106
+ return 1
107
+ return result.returncode
output.py ADDED
@@ -0,0 +1,16 @@
1
+ from __future__ import annotations
2
+
3
+
4
+ def print_step(step: str) -> None:
5
+ """统一的步骤输出格式。"""
6
+ # ******************** Run Step(build, via hook) ... *****************************
7
+ msg = f" Run Step({step}) ... "
8
+ left_stars = "*" * 20
9
+ total_width = 80
10
+ right_stars_count = total_width - len(left_stars) - len(msg)
11
+ # 防止消息过长导致右侧负填充
12
+ if right_stars_count < 0:
13
+ right_stars_count = 0
14
+ right_stars = "*" * right_stars_count
15
+ print("\n" + left_stars + msg + right_stars + "\n")
16
+
paths.py ADDED
@@ -0,0 +1,61 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ from errors import TargetResolveError
6
+
7
+
8
+ GLOBAL_CONFIG_DIRNAME = ".xt"
9
+ GLOBAL_CONFIG_FILENAME = "xt_conf.jsonc"
10
+ _PLATFORMS_DIRNAME = "platforms"
11
+
12
+
13
+ def normalize_path(path: str | Path) -> Path:
14
+ """规范化路径。"""
15
+ return Path(path).expanduser()
16
+
17
+
18
+ def build_global_config_path(home: str | Path | None = None) -> Path:
19
+ """构造全局配置文件路径。"""
20
+ base_home = normalize_path(home) if home is not None else Path.home()
21
+ return base_home / GLOBAL_CONFIG_DIRNAME / GLOBAL_CONFIG_FILENAME
22
+
23
+
24
+ def resolve_target_path(
25
+ target: str | Path,
26
+ project_dir: str | Path,
27
+ sdk_dir: str | Path,
28
+ ) -> Path:
29
+ """解析 target 路径并返回目录。"""
30
+ normalized_target = normalize_path(target)
31
+ if normalized_target.is_absolute():
32
+ candidate = normalized_target
33
+ if candidate.is_dir():
34
+ return candidate
35
+ raise TargetResolveError(f"Unable to resolve target path: {target}")
36
+
37
+ normalized_project_dir = normalize_path(project_dir)
38
+ normalized_sdk_dir = normalize_path(sdk_dir)
39
+
40
+ for base_dir in (normalized_project_dir, normalized_sdk_dir):
41
+ candidate = base_dir / _PLATFORMS_DIRNAME / normalized_target
42
+ if candidate.is_dir():
43
+ return candidate
44
+
45
+ raise TargetResolveError(f"Unable to resolve target path: {target}")
46
+
47
+
48
+ def resolve_platform_name(target: str | Path) -> str:
49
+ """从 target 名称或路径中提取平台名称。"""
50
+ normalized_target = normalize_path(target)
51
+ target_parts = normalized_target.parts
52
+
53
+ if _PLATFORMS_DIRNAME in target_parts:
54
+ platforms_index = target_parts.index(_PLATFORMS_DIRNAME)
55
+ if platforms_index + 1 < len(target_parts):
56
+ return target_parts[platforms_index + 1]
57
+
58
+ if target_parts:
59
+ return target_parts[0]
60
+
61
+ raise TargetResolveError("Unable to resolve platform name from target")
project.py ADDED
@@ -0,0 +1,28 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ from paths import normalize_path
6
+
7
+ _XMAKE_FILENAME = "xmake.lua"
8
+ _XT_MAIN_MARKER = "xt_main"
9
+
10
+
11
+ def has_xmake_project(project_dir: str | Path) -> bool:
12
+ """检查目录中是否存在 xmake.lua。"""
13
+ return _build_xmake_path(project_dir).is_file()
14
+
15
+
16
+ def has_xt_main_target(project_dir: str | Path) -> bool:
17
+ """检查工程是否包含 xt_main 标记。"""
18
+ xmake_path = _build_xmake_path(project_dir)
19
+ if not xmake_path.is_file():
20
+ return False
21
+
22
+ content = xmake_path.read_text(encoding="utf-8")
23
+ return _XT_MAIN_MARKER in content
24
+
25
+
26
+ def _build_xmake_path(project_dir: str | Path) -> Path:
27
+ """构造 xmake.lua 路径。"""
28
+ return normalize_path(project_dir) / _XMAKE_FILENAME
xmake.py ADDED
@@ -0,0 +1,92 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import subprocess
5
+ from pathlib import Path
6
+
7
+ from models import BuildContext, ConfigSource, ResolvedValue
8
+
9
+
10
+ def _xmake_file(context: BuildContext) -> Path:
11
+ """返回 xmake 主脚本路径。"""
12
+ return context.sdk_dir / "xmake.lua"
13
+
14
+
15
+ def build_command(context: BuildContext) -> list[str]:
16
+ """构造构建命令。"""
17
+ return ["xmake", "build", "-F", str(_xmake_file(context))]
18
+
19
+
20
+ def build_clean_config_command(context: BuildContext) -> list[str]:
21
+ """构造清理 xmake 配置命令。"""
22
+ return ["xmake", "f", "-c", "-F", str(_xmake_file(context))]
23
+
24
+
25
+ def build_clean_command(context: BuildContext) -> list[str]:
26
+ """构造清理命令。"""
27
+ return ["xmake", "clean", "-F", str(_xmake_file(context))]
28
+
29
+
30
+ def build_run_command(context: BuildContext, name: str | None = None) -> list[str]:
31
+ """构造运行命令。"""
32
+ command = ["xmake", "run", "-F", str(_xmake_file(context))]
33
+ if name:
34
+ command.append(name)
35
+ return command
36
+
37
+
38
+ def build_env(context: BuildContext) -> dict[str, str]:
39
+ """构造 xmake 调用环境变量。"""
40
+ env = os.environ.copy()
41
+ env.update(
42
+ {
43
+ "XT_SDK_ROOT": str(context.sdk_dir),
44
+ "XT_SDK_TARGET_PATH": str(context.target_path),
45
+ "XT_SDK_TOOLCHAIN": str(context.toolchain_path) if context.toolchain_path is not None else "",
46
+ "XT_SDK_PROJECT_PATH": str(context.project_dir),
47
+ "XT_SDK_BOARD": context.board,
48
+ }
49
+ )
50
+ return env
51
+
52
+
53
+ def run_xmake(
54
+ context: BuildContext,
55
+ command: list[str],
56
+ *,
57
+ check: bool = False,
58
+ ) -> subprocess.CompletedProcess[bytes]:
59
+ """执行 xmake 命令。"""
60
+ env = build_env(context)
61
+ if context.debug_enabled:
62
+ _print_debug_details(context, command, env)
63
+ return subprocess.run(
64
+ args=command,
65
+ cwd=context.project_dir,
66
+ env=env,
67
+ check=check,
68
+ )
69
+
70
+
71
+ def _print_debug_details(context: BuildContext, command: list[str], env: dict[str, str]) -> None:
72
+ """输出调试模式下的命令与环境信息。"""
73
+ print(f"Final command: {' '.join(command)}")
74
+ print("Environment variables:")
75
+ for name in ("XT_SDK_ROOT", "XT_SDK_TARGET_PATH", "XT_SDK_TOOLCHAIN", "XT_SDK_PROJECT_PATH", "XT_SDK_BOARD"):
76
+ value, source = _resolve_env_debug_source(context, name)
77
+ print(f" {name}={env[name]} (source={source}, value_source={value})")
78
+
79
+
80
+ def _resolve_env_debug_source(context: BuildContext, env_name: str) -> tuple[str, str]:
81
+ """返回环境变量值来源与派生来源。"""
82
+ mapping = {
83
+ "XT_SDK_ROOT": "sdk",
84
+ "XT_SDK_TARGET_PATH": "target_path",
85
+ "XT_SDK_TOOLCHAIN": "toolchain_path",
86
+ "XT_SDK_PROJECT_PATH": "project",
87
+ "XT_SDK_BOARD": "board",
88
+ }
89
+ resolved_value = None if context.resolved_values is None else context.resolved_values.get(mapping[env_name])
90
+ if isinstance(resolved_value, ResolvedValue):
91
+ return str(resolved_value.value), resolved_value.source.value
92
+ return "", ConfigSource.DERIVED.value