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.
- __init__.py +5 -0
- __main__.py +7 -0
- cli.py +342 -0
- commands/build_cmd.py +67 -0
- commands/clean_cmd.py +31 -0
- commands/config_cmd.py +202 -0
- commands/deps_cmd.py +128 -0
- commands/fullclean_cmd.py +32 -0
- config.py +356 -0
- constants.py +6 -0
- context.py +238 -0
- dependencies.py +1109 -0
- errors.py +29 -0
- hooks.py +317 -0
- models.py +107 -0
- output.py +16 -0
- paths.py +61 -0
- project.py +28 -0
- xmake.py +92 -0
- xt_cli-0.2.1.dist-info/METADATA +125 -0
- xt_cli-0.2.1.dist-info/RECORD +26 -0
- xt_cli-0.2.1.dist-info/WHEEL +5 -0
- xt_cli-0.2.1.dist-info/entry_points.txt +2 -0
- xt_cli-0.2.1.dist-info/licenses/LICENSE +202 -0
- xt_cli-0.2.1.dist-info/top_level.txt +16 -0
- xt_cli.py +10 -0
__init__.py
ADDED
__main__.py
ADDED
cli.py
ADDED
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import sys
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from typing import Sequence
|
|
7
|
+
|
|
8
|
+
from __init__ import __version__
|
|
9
|
+
from errors import XtCliError
|
|
10
|
+
|
|
11
|
+
_TASK_REGISTRY: dict[str, tuple[Callable[..., int], bool, Callable[[argparse.ArgumentParser], None] | None, str | None, str, int]] = {}
|
|
12
|
+
_SCOPE_PARSERS: dict[str, Callable[[argparse.ArgumentParser], None]] = {}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def register_scope_parser(source: str, fn: Callable[[argparse.ArgumentParser], None]) -> None:
|
|
16
|
+
"""注册 scope 级 parser。"""
|
|
17
|
+
_SCOPE_PARSERS[source] = fn
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def register_task(
|
|
21
|
+
name: str,
|
|
22
|
+
handler: Callable[..., int],
|
|
23
|
+
*,
|
|
24
|
+
independent: bool = False,
|
|
25
|
+
add_parser: Callable[[argparse.ArgumentParser], None] | None = None,
|
|
26
|
+
help: str | None = None,
|
|
27
|
+
source: str = "builtin",
|
|
28
|
+
priority: int = 100,
|
|
29
|
+
) -> None:
|
|
30
|
+
"""注册 task 及其处理函数。"""
|
|
31
|
+
_TASK_REGISTRY[name] = (handler, independent, add_parser, help, source, priority)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def add_shared_args_builtin(parser: argparse.ArgumentParser) -> None:
|
|
35
|
+
"""内置共享 option——所有 builtin task 均可访问。
|
|
36
|
+
|
|
37
|
+
当前暂无内置共享 option 需求,此处仅做示例。
|
|
38
|
+
取消注释即可激活:
|
|
39
|
+
parser.add_argument("--builtin_shared", action="store_true", help="builtin shared option example")
|
|
40
|
+
"""
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
# 注册内置 scope parser
|
|
44
|
+
register_scope_parser("builtin", add_shared_args_builtin)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _build_global_parser(add_help: bool) -> argparse.ArgumentParser:
|
|
48
|
+
"""构建仅含全局 option 的 parser(无 subparser),用于 pass 1 预解析。"""
|
|
49
|
+
parser = argparse.ArgumentParser(
|
|
50
|
+
prog="xt",
|
|
51
|
+
usage="xt [GLOBAL OPTIONS] TASK [TASK OPTIONS] ...",
|
|
52
|
+
description=f"xt command line interface\nversion: {__version__}",
|
|
53
|
+
formatter_class=argparse.RawTextHelpFormatter,
|
|
54
|
+
epilog="The sdk directory must contain .xt-sdk-version.",
|
|
55
|
+
add_help=add_help,
|
|
56
|
+
)
|
|
57
|
+
parser.add_argument("--version", action="version", version=f"xt cli {__version__}")
|
|
58
|
+
|
|
59
|
+
global_group = parser.add_argument_group("global options")
|
|
60
|
+
global_group.add_argument("--target", "-t", metavar="<target>", help="Target name or path\ne.g. windows/simulator")
|
|
61
|
+
global_group.add_argument("--sdk", metavar="<path>", help="SDK directory path\ne.g. D:/work/xt-sdk")
|
|
62
|
+
global_group.add_argument("--toolchain-path", dest="toolchain_path", metavar="<path>", help="Toolchain directory path")
|
|
63
|
+
global_group.add_argument("--project", "-p", metavar="<path>", help="Project directory path")
|
|
64
|
+
global_group.add_argument("--board", metavar="<board>", help="Board name, if add_deps(\"xt_board\")\ne.g. e837n_v01")
|
|
65
|
+
global_group.add_argument("--dirty", action="store_true", help="Allow building with dirty repositories")
|
|
66
|
+
global_group.add_argument("--no-track", action="store_true",
|
|
67
|
+
help="Skip updating xt_vers.jsonc for this build")
|
|
68
|
+
global_group.add_argument("--strict", action="store_true",
|
|
69
|
+
help="Strict mode: enforce all constraints, abort on dirty")
|
|
70
|
+
global_group.add_argument("--sync-deps", action="store_true",
|
|
71
|
+
help="Checkout repos to match xt_deps.jsonc versions before build")
|
|
72
|
+
|
|
73
|
+
return parser
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _source_rank(source: str) -> int:
|
|
77
|
+
"""返回 source 在 help 列表中的排序权重。"""
|
|
78
|
+
if source == "builtin":
|
|
79
|
+
return 0
|
|
80
|
+
if source.startswith("platform"):
|
|
81
|
+
return 1
|
|
82
|
+
if source == "project":
|
|
83
|
+
return 2
|
|
84
|
+
return 99
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _build_task_parser(add_help: bool) -> argparse.ArgumentParser:
|
|
88
|
+
"""构建带 subparser 的 parser——全局 option + 各 task 作为 subcommand。
|
|
89
|
+
|
|
90
|
+
用于:1) 预扫描 --help 显示含 subcommand 的全局帮助
|
|
91
|
+
2) 逐段解析 task_args
|
|
92
|
+
|
|
93
|
+
task 按 source 分组(builtin → platform → project),每组内按 priority 排序。
|
|
94
|
+
"""
|
|
95
|
+
parser = _build_global_parser(add_help)
|
|
96
|
+
|
|
97
|
+
# 顶层 scope options 分组(xt --help 可见)
|
|
98
|
+
for source in sorted(_SCOPE_PARSERS.keys(), key=lambda s: _source_rank(s)):
|
|
99
|
+
group = parser.add_argument_group(f"{source} shared options")
|
|
100
|
+
_SCOPE_PARSERS[source](group)
|
|
101
|
+
|
|
102
|
+
subs = parser.add_subparsers(dest="task", title="tasks")
|
|
103
|
+
sorted_tasks = sorted(
|
|
104
|
+
_TASK_REGISTRY.items(),
|
|
105
|
+
key=lambda item: (_source_rank(item[1][4]), item[1][5], item[0]),
|
|
106
|
+
)
|
|
107
|
+
for name, (_, _independent, add_parser, help_desc, source, _priority) in sorted_tasks:
|
|
108
|
+
tagged_help = f"[{source}] {help_desc}" if help_desc else f"[{source}]"
|
|
109
|
+
sub = subs.add_parser(name, help=tagged_help, add_help=True,
|
|
110
|
+
formatter_class=argparse.RawTextHelpFormatter)
|
|
111
|
+
|
|
112
|
+
if add_parser is not None:
|
|
113
|
+
add_parser(sub)
|
|
114
|
+
elif source not in _SCOPE_PARSERS:
|
|
115
|
+
sub.epilog = "No task-specific options."
|
|
116
|
+
|
|
117
|
+
return parser
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
121
|
+
"""全局 parser(仅含 global option,无 subparser),用于 pass 1。"""
|
|
122
|
+
return _build_global_parser(add_help=False)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def build_help_parser() -> argparse.ArgumentParser:
|
|
126
|
+
"""带顶层 --help + subcommand 的 parser,用于预扫描路由。"""
|
|
127
|
+
return _build_task_parser(add_help=True)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _split_task_segments(unknown_args: list[str]) -> list[tuple[str, list[str]]]:
|
|
131
|
+
"""按 task 名切分 unknown_args 为 (task_name, task_args) 段。
|
|
132
|
+
|
|
133
|
+
首个 task 之前的参数作为共享前缀追加到所有 segment。
|
|
134
|
+
"""
|
|
135
|
+
segments: list[tuple[str, list[str]]] = []
|
|
136
|
+
current_task: str | None = None
|
|
137
|
+
current_args: list[str] = []
|
|
138
|
+
pre_task_args: list[str] = []
|
|
139
|
+
|
|
140
|
+
for arg in unknown_args:
|
|
141
|
+
if arg in _TASK_REGISTRY:
|
|
142
|
+
if current_task is not None:
|
|
143
|
+
segments.append((current_task, current_args))
|
|
144
|
+
else:
|
|
145
|
+
pre_task_args = list(current_args)
|
|
146
|
+
current_task = arg
|
|
147
|
+
current_args = []
|
|
148
|
+
else:
|
|
149
|
+
current_args.append(arg)
|
|
150
|
+
|
|
151
|
+
if current_task is not None:
|
|
152
|
+
segments.append((current_task, current_args))
|
|
153
|
+
|
|
154
|
+
if pre_task_args:
|
|
155
|
+
for i in range(len(segments)):
|
|
156
|
+
segments[i] = (segments[i][0], pre_task_args + segments[i][1])
|
|
157
|
+
|
|
158
|
+
return segments
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _apply_independent(segments: list[tuple[str, list[str]]]) -> list[tuple[str, list[str]]]:
|
|
162
|
+
"""若存在 independent task,仅保留该 task 段。"""
|
|
163
|
+
for name, _task_args in segments:
|
|
164
|
+
_, independent, _add_parser, _help, _source, _priority = _TASK_REGISTRY[name]
|
|
165
|
+
if independent:
|
|
166
|
+
return [(name, _task_args)]
|
|
167
|
+
return segments
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _sort_tasks(segments: list[tuple[str, list[str]]]) -> list[tuple[str, list[str]]]:
|
|
171
|
+
"""按 priority 排序(数值越小越先执行),同优先级保持用户输入顺序(stable sort)。"""
|
|
172
|
+
return sorted(segments, key=lambda x: _TASK_REGISTRY.get(x[0], (None, None, None, None, None, 99))[5])
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _discover_hook_tasks(parsed_args: argparse.Namespace) -> None:
|
|
176
|
+
"""扫描 hook 文件,自动注册自定义 task。"""
|
|
177
|
+
from context import build_context_from_options
|
|
178
|
+
from hooks import discover_hook_tasks
|
|
179
|
+
|
|
180
|
+
try:
|
|
181
|
+
context = build_context_from_options(
|
|
182
|
+
cwd=parsed_args.project,
|
|
183
|
+
project_dir=parsed_args.project,
|
|
184
|
+
sdk_dir=parsed_args.sdk,
|
|
185
|
+
target=parsed_args.target,
|
|
186
|
+
toolchain_path=parsed_args.toolchain_path,
|
|
187
|
+
board=parsed_args.board,
|
|
188
|
+
)
|
|
189
|
+
except Exception:
|
|
190
|
+
return
|
|
191
|
+
|
|
192
|
+
hook_results, scope_parsers = discover_hook_tasks(context)
|
|
193
|
+
for task_name, add_parser, source, help_desc in hook_results:
|
|
194
|
+
if task_name not in _TASK_REGISTRY:
|
|
195
|
+
_register_hook_task(task_name, source=source, add_parser=add_parser, help_desc=help_desc)
|
|
196
|
+
for source, fn in scope_parsers.items():
|
|
197
|
+
if source not in _SCOPE_PARSERS:
|
|
198
|
+
register_scope_parser(source, fn)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _register_hook_task(task_name: str, source: str = "project", add_parser: Callable[[argparse.ArgumentParser], None] | None = None, help_desc: str | None = None) -> None:
|
|
202
|
+
"""将 hook 中的 handle_<task> 封装为标准 handler 并注册。"""
|
|
203
|
+
from constants import PRIORITY_DEFAULT
|
|
204
|
+
|
|
205
|
+
def hook_handler(context, global_args: argparse.Namespace, shared_args: argparse.Namespace, self_args: argparse.Namespace, unknown_args: list[str]) -> int:
|
|
206
|
+
from hooks import dispatch_hook
|
|
207
|
+
|
|
208
|
+
result = dispatch_hook(context, task_name, unknown_args,
|
|
209
|
+
global_args=global_args,
|
|
210
|
+
shared_args=shared_args,
|
|
211
|
+
self_args=self_args)
|
|
212
|
+
if not result.handled:
|
|
213
|
+
print(f"Unknown task: {task_name}", file=sys.stderr)
|
|
214
|
+
return 1
|
|
215
|
+
return result.returncode
|
|
216
|
+
|
|
217
|
+
register_task(task_name, hook_handler, independent=False, source=source, add_parser=add_parser,
|
|
218
|
+
help=help_desc, priority=PRIORITY_DEFAULT)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _execute_tasks(
|
|
222
|
+
parsed_args: argparse.Namespace,
|
|
223
|
+
segments: list[tuple[str, list[str]]],
|
|
224
|
+
) -> int:
|
|
225
|
+
"""按顺序执行 task 段,每段用 subparser 解析。"""
|
|
226
|
+
from context import build_context_from_options
|
|
227
|
+
|
|
228
|
+
# 先逐段解析(--help 在此阶段触发 SystemExit,避免不必要的 context 构建)
|
|
229
|
+
# 使用 bare parser(不含全局 option),确保 self_args 仅含 task 专属值
|
|
230
|
+
task_parser = argparse.ArgumentParser(add_help=False)
|
|
231
|
+
task_subs = task_parser.add_subparsers(dest="task")
|
|
232
|
+
for name, (_, _, add_parser, _, _, _) in _TASK_REGISTRY.items():
|
|
233
|
+
sub = task_subs.add_parser(name, add_help=True)
|
|
234
|
+
if add_parser:
|
|
235
|
+
add_parser(sub)
|
|
236
|
+
else:
|
|
237
|
+
sub.epilog = "No task-specific options."
|
|
238
|
+
|
|
239
|
+
parsed_segments: list[tuple[str, argparse.Namespace, argparse.Namespace, list[str]]] = []
|
|
240
|
+
shared_hook_args: list[str] = []
|
|
241
|
+
|
|
242
|
+
# 合并所有 task_args,一次性解析 shared_args——确保 shared option 不限制位置
|
|
243
|
+
combined_args: list[str] = []
|
|
244
|
+
for _, task_args in segments:
|
|
245
|
+
combined_args.extend(task_args)
|
|
246
|
+
shared_args = argparse.Namespace()
|
|
247
|
+
if _SCOPE_PARSERS:
|
|
248
|
+
scope_parser = argparse.ArgumentParser(add_help=False)
|
|
249
|
+
for source in sorted(_SCOPE_PARSERS.keys(), key=lambda s: _source_rank(s)):
|
|
250
|
+
_SCOPE_PARSERS[source](scope_parser)
|
|
251
|
+
shared_args, _ = scope_parser.parse_known_args(combined_args)
|
|
252
|
+
|
|
253
|
+
for name, task_args in segments:
|
|
254
|
+
parsed_task, hook_args = task_parser.parse_known_args([name] + task_args)
|
|
255
|
+
parsed_segments.append((name, shared_args, parsed_task, hook_args))
|
|
256
|
+
for a in hook_args:
|
|
257
|
+
if a not in shared_hook_args:
|
|
258
|
+
shared_hook_args.append(a)
|
|
259
|
+
|
|
260
|
+
# 为第一个非 independent task 构建 context
|
|
261
|
+
ctx: object = None
|
|
262
|
+
for name, _shared_args, _parsed_task, _hook_args in parsed_segments:
|
|
263
|
+
_, independent, _add_parser, _help, _source, _priority = _TASK_REGISTRY[name]
|
|
264
|
+
if not independent:
|
|
265
|
+
ctx = build_context_from_options(
|
|
266
|
+
cwd=parsed_args.project,
|
|
267
|
+
project_dir=parsed_args.project,
|
|
268
|
+
sdk_dir=parsed_args.sdk,
|
|
269
|
+
target=parsed_args.target,
|
|
270
|
+
toolchain_path=parsed_args.toolchain_path,
|
|
271
|
+
board=parsed_args.board,
|
|
272
|
+
)
|
|
273
|
+
break
|
|
274
|
+
|
|
275
|
+
for name, shared_args, parsed_task, _hook_args in parsed_segments:
|
|
276
|
+
handler, _independent, _add_parser, _help, _source, _priority = _TASK_REGISTRY[name]
|
|
277
|
+
|
|
278
|
+
# 平台专属 task 校验:source=platform/xxx 时检查当前平台匹配
|
|
279
|
+
if _source.startswith("platform/") and ctx is not None:
|
|
280
|
+
expected_platform = _source.split("/", 1)[1]
|
|
281
|
+
if getattr(ctx, "platform_name", "") != expected_platform:
|
|
282
|
+
print(f"[xt cli] Task '{name}' is only available on platform "
|
|
283
|
+
f"'{expected_platform}', current platform is '{ctx.platform_name}'")
|
|
284
|
+
continue
|
|
285
|
+
|
|
286
|
+
rc = handler(ctx, parsed_args, shared_args, parsed_task, shared_hook_args)
|
|
287
|
+
if rc != 0:
|
|
288
|
+
return rc
|
|
289
|
+
return 0
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def _main(argv: Sequence[str] | None = None) -> int:
|
|
293
|
+
args = list(sys.argv[1:] if argv is None else argv)
|
|
294
|
+
|
|
295
|
+
# 预扫描:-h/--help 出现在任何 task 名之前 → 全局 help
|
|
296
|
+
task_idx = next((i for i, a in enumerate(args) if a in _TASK_REGISTRY), len(args))
|
|
297
|
+
if any(a in {"-h", "--help"} for a in args[:task_idx]):
|
|
298
|
+
parser = build_parser()
|
|
299
|
+
parsed_args, _ = parser.parse_known_args(args)
|
|
300
|
+
_discover_hook_tasks(parsed_args)
|
|
301
|
+
build_help_parser().parse_args(args) # raises SystemExit
|
|
302
|
+
return 0
|
|
303
|
+
|
|
304
|
+
# 第一步:全局 parse_known_args(add_help=False,--help 流入 task 段)
|
|
305
|
+
parser = build_parser()
|
|
306
|
+
parsed_args, unknown_args = parser.parse_known_args(args)
|
|
307
|
+
|
|
308
|
+
# 扫 hook 注册自定义 task
|
|
309
|
+
_discover_hook_tasks(parsed_args)
|
|
310
|
+
|
|
311
|
+
segments = _split_task_segments(unknown_args)
|
|
312
|
+
|
|
313
|
+
if not segments:
|
|
314
|
+
if unknown_args:
|
|
315
|
+
if set(unknown_args) & {"-h", "--help"}:
|
|
316
|
+
build_help_parser().parse_args(args)
|
|
317
|
+
return 0
|
|
318
|
+
parser.error(f"unrecognized arguments: {' '.join(unknown_args)}")
|
|
319
|
+
parser.print_help()
|
|
320
|
+
return 0
|
|
321
|
+
|
|
322
|
+
segments = _apply_independent(segments)
|
|
323
|
+
segments = _sort_tasks(segments)
|
|
324
|
+
|
|
325
|
+
return _execute_tasks(parsed_args, segments)
|
|
326
|
+
|
|
327
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
328
|
+
try:
|
|
329
|
+
sys.stdout.reconfigure(line_buffering=True)
|
|
330
|
+
except Exception:
|
|
331
|
+
pass
|
|
332
|
+
try:
|
|
333
|
+
return _main(argv)
|
|
334
|
+
except XtCliError as e:
|
|
335
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
336
|
+
return 1
|
|
337
|
+
except KeyboardInterrupt:
|
|
338
|
+
print("KeyboardInterrupt")
|
|
339
|
+
return 1
|
|
340
|
+
|
|
341
|
+
# 导入命令模块触发 register_task 注册
|
|
342
|
+
from commands import build_cmd, clean_cmd, config_cmd, deps_cmd, fullclean_cmd # noqa: F401, E402
|
commands/build_cmd.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
|
|
5
|
+
from cli import register_task
|
|
6
|
+
from hooks import dispatch_hook
|
|
7
|
+
from models import BuildContext
|
|
8
|
+
from output import print_step
|
|
9
|
+
from xmake import build_command, run_xmake
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def handle_build_command(context: BuildContext, global_args: argparse.Namespace, shared_args: argparse.Namespace, self_args: argparse.Namespace, unknown_args: list[str]) -> int:
|
|
13
|
+
"""处理 build 命令。"""
|
|
14
|
+
# --sync-deps: 在依赖检查之前 checkout 仓库
|
|
15
|
+
sync_deps = getattr(global_args, "sync_deps", False)
|
|
16
|
+
if sync_deps:
|
|
17
|
+
from dependencies import sync_dependencies
|
|
18
|
+
dirty_allowed = getattr(global_args, "dirty", False)
|
|
19
|
+
sync_dependencies(context, dirty_allowed=dirty_allowed)
|
|
20
|
+
|
|
21
|
+
# 在 dispatch_hook 之前检查依赖版本(防止 hook 绕过)
|
|
22
|
+
from dependencies import check_dependencies, _print_warnings_box
|
|
23
|
+
dirty_allowed = getattr(global_args, "dirty", False)
|
|
24
|
+
strict = getattr(global_args, "strict", False)
|
|
25
|
+
passed, warnings = check_dependencies(context, dirty_allowed=dirty_allowed, strict=strict)
|
|
26
|
+
if not passed:
|
|
27
|
+
return 1
|
|
28
|
+
|
|
29
|
+
hook_result = dispatch_hook(context, "build", unknown_args, global_args=global_args, shared_args=shared_args, self_args=self_args)
|
|
30
|
+
if hook_result.handled:
|
|
31
|
+
return hook_result.returncode
|
|
32
|
+
|
|
33
|
+
from hooks import run_lifecycle_hook
|
|
34
|
+
|
|
35
|
+
before_ret = run_lifecycle_hook(context, "before_build", unknown_args, global_args, shared_args, self_args)
|
|
36
|
+
if before_ret != 0:
|
|
37
|
+
return before_ret
|
|
38
|
+
|
|
39
|
+
print_step("build")
|
|
40
|
+
if warnings:
|
|
41
|
+
_print_warnings_box(warnings)
|
|
42
|
+
result = run_xmake(context, build_command(context))
|
|
43
|
+
|
|
44
|
+
run_lifecycle_hook(context, "after_build", unknown_args, global_args, shared_args, self_args)
|
|
45
|
+
|
|
46
|
+
# 在 after_build hook 之后更新版本追踪(反映最终仓库状态)
|
|
47
|
+
if result.returncode == 0:
|
|
48
|
+
from dependencies import update_dependencies, _is_track_enabled
|
|
49
|
+
no_track = getattr(global_args, "no_track", False)
|
|
50
|
+
if not no_track and _is_track_enabled(context):
|
|
51
|
+
update_dependencies(context)
|
|
52
|
+
|
|
53
|
+
return result.returncode
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def add_args_build(parser: argparse.ArgumentParser) -> None:
|
|
57
|
+
"""注册 build 专属 option 说明。"""
|
|
58
|
+
parser.epilog = (
|
|
59
|
+
"Global flags affecting build:\n"
|
|
60
|
+
" --dirty Allow building with dirty repositories (always warns)\n"
|
|
61
|
+
" --no-track Skip updating xt_vers.jsonc for this build\n"
|
|
62
|
+
" --strict Enforce all constraints as error, abort on dirty\n"
|
|
63
|
+
" --sync-deps Checkout repos to match xt_deps.jsonc versions before build"
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
register_task("build", handle_build_command, independent=False, add_parser=add_args_build, help="Build the project", priority=20)
|
commands/clean_cmd.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
|
|
5
|
+
from cli import register_task
|
|
6
|
+
from hooks import dispatch_hook
|
|
7
|
+
from models import BuildContext
|
|
8
|
+
from output import print_step
|
|
9
|
+
from xmake import build_clean_command, run_xmake
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def handle_clean_command(context: BuildContext, global_args: argparse.Namespace, shared_args: argparse.Namespace, self_args: argparse.Namespace, unknown_args: list[str]) -> int:
|
|
13
|
+
"""处理 clean 命令。"""
|
|
14
|
+
hook_result = dispatch_hook(context, "clean", unknown_args, global_args=global_args, shared_args=shared_args, self_args=self_args)
|
|
15
|
+
if hook_result.handled:
|
|
16
|
+
return hook_result.returncode
|
|
17
|
+
|
|
18
|
+
from hooks import run_lifecycle_hook
|
|
19
|
+
|
|
20
|
+
before_ret = run_lifecycle_hook(context, "before_clean", unknown_args, global_args, shared_args, self_args)
|
|
21
|
+
if before_ret != 0:
|
|
22
|
+
return before_ret
|
|
23
|
+
|
|
24
|
+
print_step("clean")
|
|
25
|
+
result = run_xmake(context, build_clean_command(context))
|
|
26
|
+
|
|
27
|
+
run_lifecycle_hook(context, "after_clean", unknown_args, global_args, shared_args, self_args)
|
|
28
|
+
return result.returncode
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
register_task("clean", handle_clean_command, independent=False, help="Clean build artifacts", priority=10)
|
commands/config_cmd.py
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from cli import register_task
|
|
7
|
+
from config import get_config_value, reset_config_key, set_config_value
|
|
8
|
+
from config import _resolve_config_store
|
|
9
|
+
from constants import PRIORITY_INDEPENDENT
|
|
10
|
+
from errors import CliUsageError
|
|
11
|
+
from models import BuildContext
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def add_args_config(parser: argparse.ArgumentParser) -> None:
|
|
15
|
+
"""注册 config 专属 option。"""
|
|
16
|
+
action_group = parser.add_mutually_exclusive_group()
|
|
17
|
+
action_group.add_argument("--get", dest="get_key", metavar="<key>", help="Get config value")
|
|
18
|
+
action_group.add_argument("--reset", dest="reset_key", metavar="<key>", help="Reset config value")
|
|
19
|
+
action_group.add_argument("--show", dest="show_config", action="store_true", help="Show all config values")
|
|
20
|
+
parser.add_argument("--platform", metavar="<platform>", help="Select platform scope")
|
|
21
|
+
parser.add_argument("--local", action="store_true", help="Use local config file")
|
|
22
|
+
parser.add_argument(
|
|
23
|
+
"--debug", dest="config_debug", metavar="<0|1>", choices=("0", "1"),
|
|
24
|
+
help="Set debug flag, 0 disables and 1 enables debug output",
|
|
25
|
+
)
|
|
26
|
+
parser.add_argument(
|
|
27
|
+
"--deps-on-version-fail", dest="deps_on_version_fail",
|
|
28
|
+
metavar="<warn|error|ignore>", choices=("warn", "error", "ignore"),
|
|
29
|
+
help="Default behavior for version constraint mismatch",
|
|
30
|
+
)
|
|
31
|
+
parser.add_argument(
|
|
32
|
+
"--deps-on-target-fail", dest="deps_on_target_fail",
|
|
33
|
+
metavar="<warn|error|ignore>", choices=("warn", "error", "ignore"),
|
|
34
|
+
help="Default behavior for target constraint mismatch",
|
|
35
|
+
)
|
|
36
|
+
parser.add_argument(
|
|
37
|
+
"--deps-track", dest="deps_track",
|
|
38
|
+
metavar="<true|false>", choices=("true", "false"),
|
|
39
|
+
help="Enable or disable xt_vers.jsonc tracking",
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def handle_config_command(context: BuildContext, global_args: argparse.Namespace, shared_args: argparse.Namespace, self_args: argparse.Namespace, unknown_args: list[str]) -> int:
|
|
44
|
+
"""处理 config 命令。"""
|
|
45
|
+
cwd = Path.cwd()
|
|
46
|
+
|
|
47
|
+
if self_args.get_key is not None:
|
|
48
|
+
value, source = get_config_value(
|
|
49
|
+
key=self_args.get_key,
|
|
50
|
+
platform=self_args.platform,
|
|
51
|
+
use_local=self_args.local,
|
|
52
|
+
cwd=cwd,
|
|
53
|
+
)
|
|
54
|
+
print(f"{value} (from {source})")
|
|
55
|
+
return 0
|
|
56
|
+
|
|
57
|
+
if self_args.reset_key is not None:
|
|
58
|
+
source = reset_config_key(
|
|
59
|
+
key=self_args.reset_key,
|
|
60
|
+
platform=self_args.platform,
|
|
61
|
+
use_local=self_args.local,
|
|
62
|
+
cwd=cwd,
|
|
63
|
+
)
|
|
64
|
+
print(f"{self_args.reset_key} reset (from {source})")
|
|
65
|
+
return 0
|
|
66
|
+
|
|
67
|
+
set_key, set_value = _resolve_set_action(global_args, self_args)
|
|
68
|
+
if set_key is not None and set_value is not None:
|
|
69
|
+
source = set_config_value(
|
|
70
|
+
key=set_key,
|
|
71
|
+
value=set_value,
|
|
72
|
+
platform=self_args.platform if self_args else None,
|
|
73
|
+
use_local=self_args.local if self_args else False,
|
|
74
|
+
cwd=cwd,
|
|
75
|
+
)
|
|
76
|
+
stored_value, _ = get_config_value(
|
|
77
|
+
key=set_key,
|
|
78
|
+
platform=self_args.platform if self_args else None,
|
|
79
|
+
use_local=self_args.local if self_args else False,
|
|
80
|
+
cwd=cwd,
|
|
81
|
+
)
|
|
82
|
+
print(f"{set_key} set to {stored_value} (from {source})")
|
|
83
|
+
return 0
|
|
84
|
+
|
|
85
|
+
_show_all_config(cwd)
|
|
86
|
+
return 0
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _resolve_set_action(parsed_global: argparse.Namespace, parsed_task) -> tuple[str | None, str | None]:
|
|
90
|
+
"""解析设置动作。"""
|
|
91
|
+
set_actions = [
|
|
92
|
+
("sdk", parsed_global.sdk),
|
|
93
|
+
("target", parsed_global.target),
|
|
94
|
+
("board", parsed_global.board),
|
|
95
|
+
("debug", getattr(parsed_task, "config_debug", None) if parsed_task else None),
|
|
96
|
+
("toolchain-path", parsed_global.toolchain_path),
|
|
97
|
+
("deps.on_version_fail", getattr(parsed_task, "deps_on_version_fail", None) if parsed_task else None),
|
|
98
|
+
("deps.on_target_fail", getattr(parsed_task, "deps_on_target_fail", None) if parsed_task else None),
|
|
99
|
+
("deps.track", getattr(parsed_task, "deps_track", None) if parsed_task else None),
|
|
100
|
+
]
|
|
101
|
+
selected_actions = [(key, value) for key, value in set_actions if value is not None]
|
|
102
|
+
if len(selected_actions) > 1:
|
|
103
|
+
raise CliUsageError("Only one config set action is allowed")
|
|
104
|
+
if not selected_actions:
|
|
105
|
+
return None, None
|
|
106
|
+
return selected_actions[0]
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _show_all_config(cwd: Path) -> None:
|
|
110
|
+
"""展示所有配置(分区:local / global / defaults)。"""
|
|
111
|
+
from config import _LOCAL_CONFIG_FILENAME
|
|
112
|
+
|
|
113
|
+
local_store = _resolve_config_store(use_local=True, cwd=cwd, home=None)
|
|
114
|
+
global_store = _resolve_config_store(use_local=False, cwd=cwd, home=None)
|
|
115
|
+
|
|
116
|
+
local_data = local_store.load_or_default()
|
|
117
|
+
global_data = global_store.load_or_default()
|
|
118
|
+
|
|
119
|
+
local_path = cwd / _LOCAL_CONFIG_FILENAME
|
|
120
|
+
global_path = global_store.path
|
|
121
|
+
|
|
122
|
+
print(f"--- Local config ({local_path}) ---")
|
|
123
|
+
if local_data:
|
|
124
|
+
_print_config_section(local_data)
|
|
125
|
+
else:
|
|
126
|
+
print("(empty)")
|
|
127
|
+
|
|
128
|
+
print(f"\n--- Global config ({global_path}) ---")
|
|
129
|
+
if global_data:
|
|
130
|
+
_print_config_section(global_data)
|
|
131
|
+
else:
|
|
132
|
+
print("(empty)")
|
|
133
|
+
|
|
134
|
+
_print_defaults_section(local_data, global_data)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _print_config_section(data: dict) -> None:
|
|
138
|
+
"""输出配置段内容。"""
|
|
139
|
+
for key in sorted(data):
|
|
140
|
+
if key == "config_version":
|
|
141
|
+
continue
|
|
142
|
+
value = data[key]
|
|
143
|
+
if key == "toolchains" and isinstance(value, dict):
|
|
144
|
+
for platform in sorted(value):
|
|
145
|
+
print(f"toolchain-path ({platform}) = {value[platform]}")
|
|
146
|
+
elif key == "deps_configs" and isinstance(value, dict):
|
|
147
|
+
for sub_key in sorted(value):
|
|
148
|
+
print(f"deps.{sub_key} = {value[sub_key]}")
|
|
149
|
+
else:
|
|
150
|
+
print(f"{key} = {value}")
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
_REGISTERED_KEYS = [
|
|
154
|
+
"sdk", "target", "board", "debug", "toolchain-path",
|
|
155
|
+
"deps.on_version_fail", "deps.on_target_fail", "deps.track",
|
|
156
|
+
]
|
|
157
|
+
|
|
158
|
+
_DEFAULT_VALUES = {
|
|
159
|
+
"target": "windows/simulator",
|
|
160
|
+
"board": "(not set)",
|
|
161
|
+
"debug": 0,
|
|
162
|
+
"deps.on_version_fail": "warn",
|
|
163
|
+
"deps.on_target_fail": "warn",
|
|
164
|
+
"deps.track": False,
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _print_defaults_section(local_data: dict, global_data: dict) -> None:
|
|
169
|
+
"""输出默认值段(仅显示本地和全局中均未设置的键)。"""
|
|
170
|
+
lines: list[str] = []
|
|
171
|
+
for key in _REGISTERED_KEYS:
|
|
172
|
+
if key == "toolchain-path":
|
|
173
|
+
continue
|
|
174
|
+
value = _resolve_best_value(key, local_data, global_data)
|
|
175
|
+
if value is not None:
|
|
176
|
+
continue
|
|
177
|
+
default = _DEFAULT_VALUES.get(key, "(not set)")
|
|
178
|
+
lines.append(f"{key} = {default}")
|
|
179
|
+
|
|
180
|
+
if lines:
|
|
181
|
+
print("\n--- Defaults ---")
|
|
182
|
+
for line in lines:
|
|
183
|
+
print(line)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _resolve_best_value(key: str, local_data: dict, global_data: dict):
|
|
187
|
+
"""从 local/global 数据中解析最佳值。"""
|
|
188
|
+
if key.startswith("deps."):
|
|
189
|
+
sub_key = key[5:]
|
|
190
|
+
for data in (local_data, global_data):
|
|
191
|
+
deps = data.get("deps_configs")
|
|
192
|
+
if isinstance(deps, dict) and sub_key in deps:
|
|
193
|
+
return deps[sub_key]
|
|
194
|
+
return None
|
|
195
|
+
|
|
196
|
+
for data in (local_data, global_data):
|
|
197
|
+
if key in data:
|
|
198
|
+
return data[key]
|
|
199
|
+
return None
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
register_task("config", handle_config_command, independent=True, add_parser=add_args_config, help="Configure settings", priority=PRIORITY_INDEPENDENT)
|