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
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
|